Skip to content

Commit bce4197

Browse files
committed
feat: add parameter optimization page
1 parent 6e9f211 commit bce4197

3 files changed

Lines changed: 766 additions & 0 deletions

File tree

TheGreenEpochWeb/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ScenariosPage } from "./pages/ScenariosPage";
55
import { LiveSimPage } from "./pages/LiveSimPage";
66
import { ResultsPage } from "./pages/ResultsPage";
77
import { RunsPage } from "./pages/RunsPage";
8+
import { OptimizePage } from "./pages/OptimizePage";
89

910
const THEME_KEY = "thegreenepoch-theme";
1011

@@ -65,6 +66,14 @@ function Layout(props: { children?: JSX.Element }) {
6566
>
6667
Runs
6768
</A>
69+
<A
70+
href="/optimize"
71+
class="px-3 py-1.5 rounded-lg no-underline transition-colors"
72+
activeClass="bg-accent-subtle text-accent font-medium"
73+
inactiveClass="text-fg-subtle hover:text-fg-body hover:bg-white/5"
74+
>
75+
Optimize
76+
</A>
6877
<button
6978
onClick={toggleTheme}
7079
class="ml-2 p-1.5 rounded-lg text-fg-subtle hover:text-fg-body hover:bg-white/5 transition-colors"
@@ -121,6 +130,7 @@ export function App() {
121130
<Route path="/simulate/:id" component={LiveSimPage} />
122131
<Route path="/results/:id" component={ResultsPage} />
123132
<Route path="/runs" component={RunsPage} />
133+
<Route path="/optimize" component={OptimizePage} />
124134
</Router>
125135
</Show>
126136
);
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import type { FullProfile, SimConfig, CO2Timeline, Scenario } from "../types";
2+
import { simulateStepwise, buildResult } from "./simulation";
3+
4+
export interface SweepPoint {
5+
thetaPause: number;
6+
thetaResume: number;
7+
actualOverheadPct: number;
8+
co2SavingsPct: number;
9+
score: number;
10+
numPauses: number;
11+
totalEmissionsKgco2: number;
12+
baselineEmissionsKgco2: number;
13+
withinBudget: boolean;
14+
stopReason: string;
15+
completed: boolean;
16+
iteration: number;
17+
}
18+
19+
export interface SweepOptions {
20+
thetaPauseMin: number;
21+
thetaPauseMax: number;
22+
thetaPauseStep: number;
23+
hysteresisMode: "ratio" | "offset";
24+
hysteresisValue: number;
25+
overheadBudgetPct: number;
26+
}
27+
28+
export interface AdaptiveOptions {
29+
thetaPauseMax: number;
30+
overheadBudgetPct: number;
31+
resolution: number;
32+
maxIterations: number;
33+
minStep: number;
34+
shrinkFactor: number;
35+
}
36+
37+
export interface AdaptiveResult {
38+
points: SweepPoint[];
39+
best: SweepPoint | null;
40+
iterations: number;
41+
}
42+
43+
export function runSweep(
44+
profile: FullProfile,
45+
timeline: CO2Timeline,
46+
scenario: Scenario,
47+
options: SweepOptions,
48+
startTimeIdx = 0,
49+
): SweepPoint[] {
50+
const startTime = scenario.startTimes[startTimeIdx] || "01-01";
51+
52+
const shared = {
53+
scenarioDescription: scenario.description,
54+
region: scenario.region,
55+
historicalYears: scenario.historicalYears,
56+
startTime,
57+
overheadBudgetPct: options.overheadBudgetPct,
58+
};
59+
60+
const baselineLast = runBaseline(profile, shared, timeline);
61+
62+
const points: SweepPoint[] = [];
63+
for (let tp = options.thetaPauseMin; tp <= options.thetaPauseMax; tp = round(tp + options.thetaPauseStep)) {
64+
const tr = options.hysteresisMode === "ratio"
65+
? round(tp * options.hysteresisValue)
66+
: Math.max(0, round(tp - options.hysteresisValue));
67+
68+
const pt = evaluate(profile, { ...shared, thetaPause: tp, thetaResume: tr }, timeline, baselineLast, 0);
69+
if (pt) points.push(pt);
70+
}
71+
72+
return points;
73+
}
74+
75+
export function adaptiveSweep(
76+
profile: FullProfile,
77+
timeline: CO2Timeline,
78+
scenario: Scenario,
79+
options: AdaptiveOptions,
80+
startTimeIdx = 0,
81+
onIteration?: (iteration: number, points: SweepPoint[], best: SweepPoint | null) => void,
82+
): AdaptiveResult {
83+
const startTime = scenario.startTimes[startTimeIdx] || "01-01";
84+
85+
const shared = {
86+
scenarioDescription: scenario.description,
87+
region: scenario.region,
88+
historicalYears: scenario.historicalYears,
89+
startTime,
90+
overheadBudgetPct: options.overheadBudgetPct,
91+
};
92+
93+
const baselineLast = runBaseline(profile, shared, timeline);
94+
95+
const allPoints: SweepPoint[] = [];
96+
let iterPoints: SweepPoint[] = [];
97+
let best: SweepPoint | null = null;
98+
99+
let tpMin = 10;
100+
let tpMax = options.thetaPauseMax;
101+
let trMin = 0;
102+
let trMax = options.thetaPauseMax;
103+
104+
for (let iter = 0; iter < options.maxIterations; iter++) {
105+
const stepTp = Math.max((tpMax - tpMin) / (options.resolution - 1), 1);
106+
const stepTr = Math.max((trMax - trMin) / (options.resolution - 1), 1);
107+
108+
if (stepTp < options.minStep && stepTr < options.minStep) break;
109+
110+
iterPoints = [];
111+
112+
for (let tpi = 0; tpi < options.resolution; tpi++) {
113+
const tp = round(tpMin + tpi * stepTp);
114+
if (tp > options.thetaPauseMax) break;
115+
116+
const trCount = Math.max(2, Math.round(((tp - trMin) / (trMax - trMin)) * options.resolution));
117+
for (let tri = 0; tri < trCount; tri++) {
118+
const tr = round(trMin + tri * (tp - trMin) / Math.max(trCount - 1, 1));
119+
if (tr > tp) break;
120+
121+
const pt = evaluate(profile, { ...shared, thetaPause: tp, thetaResume: tr }, timeline, baselineLast, iter);
122+
if (pt) iterPoints.push(pt);
123+
}
124+
}
125+
126+
allPoints.push(...iterPoints);
127+
128+
const iterBest = findBest(iterPoints, options.overheadBudgetPct);
129+
if (iterBest && (!best || iterBest.score > best.score)) {
130+
best = iterBest;
131+
}
132+
133+
if (onIteration) onIteration(iter, iterPoints, best);
134+
135+
if (iter === options.maxIterations - 1) break;
136+
137+
if (!best) {
138+
tpMin = Math.max(10, tpMin * 0.8);
139+
tpMax = Math.min(options.thetaPauseMax, tpMax * 0.9);
140+
trMin = 0;
141+
trMax = tpMax;
142+
continue;
143+
}
144+
145+
const span = tpMax - tpMin;
146+
const newMin = Math.max(10, best.thetaPause - span * options.shrinkFactor / 2);
147+
const newMax = Math.min(options.thetaPauseMax, best.thetaPause + span * options.shrinkFactor / 2);
148+
149+
tpMin = round(newMin);
150+
tpMax = round(newMax);
151+
trMin = round(Math.max(0, best.thetaResume - span * options.shrinkFactor / 2));
152+
trMax = round(Math.min(tpMax, best.thetaResume + span * options.shrinkFactor / 2));
153+
trMin = Math.max(0, trMin);
154+
trMax = Math.max(trMin + options.minStep, trMax);
155+
}
156+
157+
return { points: allPoints, best, iterations: Math.min(options.maxIterations, allPoints.length > 0 ? allPoints[allPoints.length - 1].iteration + 1 : 0) };
158+
}
159+
160+
function runBaseline(profile: FullProfile, shared: { scenarioDescription: string; region: string; historicalYears: number[]; startTime: string; overheadBudgetPct: number }, timeline: CO2Timeline) {
161+
const baselineProgress: import("../types").SimProgress[] = [];
162+
for (const p of simulateStepwise(profile, { ...shared, thetaPause: Infinity, thetaResume: 0 }, timeline)) {
163+
baselineProgress.push(p);
164+
}
165+
return baselineProgress[baselineProgress.length - 1];
166+
}
167+
168+
function evaluate(
169+
profile: FullProfile,
170+
config: SimConfig,
171+
timeline: CO2Timeline,
172+
baselineLast: import("../types").SimProgress,
173+
iteration: number,
174+
): SweepPoint | null {
175+
let lastProgress: import("../types").SimProgress | null = null;
176+
for (const p of simulateStepwise(profile, config, timeline)) {
177+
lastProgress = p;
178+
}
179+
if (!lastProgress) return null;
180+
181+
const meta = buildResult(profile, config, lastProgress, baselineLast);
182+
const actualOverheadPct = meta.actualOverheadPct;
183+
const co2SavingsPct = meta.baselineEmissionsKgco2 > 0
184+
? (meta.baselineEmissionsKgco2 - meta.totalEmissionsKgco2) / meta.baselineEmissionsKgco2 * 100
185+
: 0;
186+
187+
return {
188+
thetaPause: config.thetaPause === Infinity ? 9999 : config.thetaPause,
189+
thetaResume: config.thetaResume,
190+
actualOverheadPct,
191+
co2SavingsPct,
192+
score: co2SavingsPct / Math.max(actualOverheadPct, 0.001),
193+
numPauses: meta.numPauses,
194+
totalEmissionsKgco2: meta.totalEmissionsKgco2,
195+
baselineEmissionsKgco2: meta.baselineEmissionsKgco2,
196+
withinBudget: meta.withinOverheadBudget,
197+
stopReason: meta.stopReason,
198+
completed: meta.completed,
199+
iteration,
200+
};
201+
}
202+
203+
function findBest(points: SweepPoint[], budget: number): SweepPoint | null {
204+
const valid = points.filter(r => r.withinBudget && r.co2SavingsPct > 0);
205+
if (valid.length === 0) return null;
206+
return valid.reduce((a, b) => (a.score > b.score ? a : b));
207+
}
208+
209+
function round(n: number): number {
210+
return Math.round(n * 100) / 100;
211+
}

0 commit comments

Comments
 (0)