Skip to content

Commit c766ea5

Browse files
committed
Top Bar Scenario Simulation Sequence
1 parent fa40fef commit c766ea5

3 files changed

Lines changed: 128 additions & 26 deletions

File tree

public/demo/demo.gif

27.7 MB
Loading

src/components/cockpit/AppShell.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export function AppShell() {
188188
</div>
189189
<button
190190
className="freq-inject"
191+
disabled={scenarioRun?.active}
191192
onClick={() => {
192193
runTimedScenario("frequency_islanding", draftFrequencyHz);
193194
setFrequencyOpen(false);
@@ -203,6 +204,7 @@ export function AppShell() {
203204

204205
<button
205206
className="command-button"
207+
disabled={scenarioRun?.active}
206208
onClick={() => runTimedScenario("topology_split")}
207209
type="button"
208210
>
@@ -211,6 +213,7 @@ export function AppShell() {
211213
</button>
212214
<button
213215
className="command-button"
216+
disabled={scenarioRun?.active}
214217
onClick={() => runTimedScenario("generation_derate")}
215218
type="button"
216219
>
@@ -219,6 +222,7 @@ export function AppShell() {
219222
</button>
220223
<button
221224
className="command-button"
225+
disabled={scenarioRun?.active}
222226
onClick={() => runTimedScenario("ols_overload")}
223227
type="button"
224228
>
@@ -227,6 +231,7 @@ export function AppShell() {
227231
</button>
228232
<button
229233
className="command-button"
234+
disabled={scenarioRun?.active}
230235
onClick={() => runTimedScenario("ogs_surplus")}
231236
type="button"
232237
>

src/lib/ads/store.ts

Lines changed: 123 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -652,49 +652,146 @@ export const useAdsStore = create<AdsStore>((set, get) => ({
652652
if (get().scenarioRun?.active) return;
653653

654654
const title = scenarioTitle(kind);
655-
set({
656-
scenarioRun: {
657-
active: true,
658-
title,
659-
step: 0,
660-
total: 2,
661-
message: "Preparing timed scenario. This uses the same application command path as operator actions.",
662-
},
663-
eventLog: [`Scenario started: ${title}. Step timing ${sequenceDelayMs / 1000}s.`, ...get().eventLog].slice(0, 120),
664-
});
655+
const previewDelayMs = 1000;
656+
const actionDelayMs = 800;
657+
658+
const totalByKind: Record<ScenarioKind, number> = {
659+
manual_relief: 2,
660+
topology_split: 2,
661+
generation_derate: 2,
662+
frequency_islanding: 5,
663+
ols_overload: 2,
664+
ogs_surplus: 6,
665+
};
665666

666-
await wait();
667+
const total = totalByKind[kind] ?? 2;
668+
let step = 0;
669+
670+
const emit = (message: string, log?: string) => {
671+
step += 1;
672+
set({
673+
scenarioRun: {
674+
active: true,
675+
title,
676+
step,
677+
total,
678+
message,
679+
},
680+
eventLog: [log ?? `${title}: ${message}`, ...get().eventLog].slice(0, 120),
681+
});
682+
};
683+
684+
const clearPreview = () => {
685+
if (get().hoverObjectId) get().setHoverObject(null);
686+
};
687+
688+
const previewObject = async (objectId: string, message: string) => {
689+
emit(
690+
`Preview: ${message}`,
691+
`Scenario preview: hover ${objectId}. Trip Matrix row is shown before the action is executed.`,
692+
);
693+
get().setHoverObject(objectId);
694+
await wait(previewDelayMs);
695+
};
696+
697+
const executeObjectOpen = async (objectId: string, message: string) => {
698+
emit(
699+
`Execute: ${message}`,
700+
`Scenario action: execute ${objectId} through the same SLD click path.`,
701+
);
702+
const state = get().objectStates[objectId] ?? "closed";
703+
if (state !== "open" && state !== "failed") {
704+
get().toggleObject(objectId);
705+
} else {
706+
set({
707+
eventLog: [`Scenario action skipped: ${objectId} is already open.`, ...get().eventLog].slice(0, 120),
708+
});
709+
}
710+
clearPreview();
711+
await wait(actionDelayMs);
712+
};
713+
714+
const applyFrequencyPreview = async (hz: number, message: string) => {
715+
emit(
716+
message,
717+
`Frequency guided preview: ${hz.toFixed(2)} Hz. Rebuilding Trip Matrix without executing load trip yet.`,
718+
);
719+
const current = get();
720+
const tripMatrix = buildMatrix(
721+
current.feeders,
722+
current.objectStates,
723+
current.contingencyRules,
724+
current.sourceMw,
725+
current.minReserveMw,
726+
hz,
727+
);
728+
set({
729+
frequencyHz: hz,
730+
...matrixState(tripMatrix),
731+
decision: tripMatrix.baseDecision,
732+
hoverDecision: null,
733+
hoverObjectId: null,
734+
});
735+
await wait(actionDelayMs);
736+
};
737+
738+
const applyScenarioAfterPreview = async (scenarioKind: ScenarioKind, scenarioValue: number | undefined, message: string) => {
739+
emit(message, `Scenario action: apply ${scenarioKind} after visual preview.`);
740+
clearPreview();
741+
get().injectScenario(scenarioKind, scenarioValue);
742+
await wait(actionDelayMs);
743+
};
667744

668745
set({
669746
scenarioRun: {
670747
active: true,
671748
title,
672-
step: 1,
673-
total: 2,
674-
message:
675-
kind === "topology_split"
676-
? "Operator action: open Bus B coupler."
677-
: kind === "frequency_islanding"
678-
? `Parameter action: inject ${(value ?? 48.25).toFixed(2)} Hz.`
679-
: "Scenario action: apply system event and rebuild Trip Matrix.",
749+
step: 0,
750+
total,
751+
message: "Starting guided scenario. Hover preview is shown first, then the same action path as SLD click is executed.",
680752
},
753+
eventLog: [`Scenario started: ${title}. Preview delay 1.0s, action delay ${(actionDelayMs / 1000).toFixed(1)}s.`, ...get().eventLog].slice(0, 120),
681754
});
682755

756+
await wait(300);
757+
683758
if (kind === "topology_split") {
684-
if (get().objectStates.LINE_COUPLER !== "open") get().toggleObject("LINE_COUPLER");
759+
await previewObject("LINE_COUPLER", "Bus B coupler contingency. Watch Trip Matrix arming before the coupler opens.");
760+
await executeObjectOpen("LINE_COUPLER", "Open Bus B coupler as an operator CB action.");
761+
} else if (kind === "ogs_surplus") {
762+
await previewObject("LINE_AC", "Open Line A-C to start separating the C area.");
763+
await executeObjectOpen("LINE_AC", "Trip Line A-C.");
764+
765+
await previewObject("LINE_BC", "Open Line B-C so C is separated from the A/B network.");
766+
await executeObjectOpen("LINE_BC", "Trip Line B-C.");
767+
768+
await previewObject("IBT_C", "Trip IBT C / grid support. This tests whether the C area becomes a true island and whether OGS/runback is required.");
769+
await executeObjectOpen("IBT_C", "Trip IBT C as the final island-forming contingency.");
770+
} else if (kind === "generation_derate") {
771+
await previewObject("GEN_C2", "KIT C2 derate scenario. Generator stays online but available MW is reduced.");
772+
await applyScenarioAfterPreview("generation_derate", value, "Apply KIT C2 derate and rebuild PowerFlowLite + Trip Matrix.");
773+
} else if (kind === "ols_overload") {
774+
await previewObject("IBT_C", "IBT C overload scenario. ADS should show why the load-side relief target is selected.");
775+
await applyScenarioAfterPreview("ols_overload", value, "Apply IBT C overload test case after preview.");
776+
} else if (kind === "frequency_islanding") {
777+
const finalHz = value ?? 48.25;
778+
await applyFrequencyPreview(50.0, "Frequency normal reference: 50.00 Hz. No frequency-based ADS action yet.");
779+
await applyFrequencyPreview(49.0, "Frequency enters UFLS coordination zone. ADS prepares but does not blindly trip.");
780+
await applyFrequencyPreview(48.4, "Frequency approaches islanding threshold. Trip Matrix is recalculated from the current state.");
781+
await applyFrequencyPreview(finalHz, `Final frequency injection ${finalHz.toFixed(2)} Hz. ADS preview is now visible before execution.`);
782+
await applyScenarioAfterPreview("frequency_islanding", finalHz, `Execute frequency injection at ${finalHz.toFixed(2)} Hz.`);
685783
} else {
686-
get().injectScenario(kind, value);
784+
await applyScenarioAfterPreview(kind, value, "Apply timed scenario.");
687785
}
688786

689-
await wait();
690-
787+
clearPreview();
691788
set({
692789
scenarioRun: {
693790
active: false,
694791
title,
695-
step: 2,
696-
total: 2,
697-
message: "Scenario complete. Review Power Flow cards, Trip Matrix, and Event Log.",
792+
step: total,
793+
total,
794+
message: "Guided scenario complete. Review SLD response, Power Flow card, Trip Matrix, and Event Log.",
698795
},
699796
eventLog: [`Scenario complete: ${title}.`, ...get().eventLog].slice(0, 120),
700797
});

0 commit comments

Comments
 (0)