Skip to content

Add PST tap position re-simulation support - #78

Merged
marota merged 22 commits into
mainfrom
claude/add-pst-tap-adjustment-lTZRl
Apr 11, 2026
Merged

marota merged 22 commits into
mainfrom
claude/add-pst-tap-adjustment-lTZRl

Conversation

@marota

@marota marota commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds support for re-simulating Phase Shift Transformer (PST) actions with different tap positions. Users can now edit the tap position of PST actions and re-simulate them to see the impact on the grid, similar to the existing MW adjustment feature for load shedding/curtailment actions.

Key Changes

Backend (expert_backend)

  • New method _compute_pst_details() in recommender_service.py: Extracts PST tap information from actions and retrieves tap bounds (low_tap, high_tap) from the network manager
  • Enhanced simulate_manual_action(): Added target_tap parameter to support PST tap re-simulation with automatic clamping to valid bounds
  • PST details enrichment: PST information is now computed and included in action enrichment pipeline, similar to load shedding and curtailment details
  • Updated API endpoint: ManualActionRequest now accepts optional target_tap parameter

Frontend (React/TypeScript)

  • New state management: Added cardEditTap state to track per-action editable tap positions
  • New handler handleResimulateTap(): Processes PST tap re-simulation requests and updates action results
  • UI component for PST details: Displays PST name, current tap position, valid tap range, editable input field, and re-simulate button
  • Type definitions: Added PstDetail interface with pst_name, tap_position, low_tap, and high_tap fields
  • API integration: Extended simulateManualAction() to accept optional targetTap parameter

Standalone Interface

  • Mirrors all frontend changes for consistency in the standalone HTML interface
  • Includes PST details display and re-simulation functionality

Implementation Details

  • PST tap bounds are retrieved from the network via get_pst_tap_info() to ensure valid ranges
  • Tap values are automatically clamped to [low_tap, high_tap] during re-simulation
  • The UI provides visual feedback with tap range display and disabled state during simulation
  • Edit inputs are cleared after successful re-simulation to reflect updated results from the backend
  • Consistent styling with existing load shedding/curtailment detail cards (purple theme)

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC

claude and others added 22 commits April 9, 2026 12:36
Allow users to change tap values for PST actions within the available
tap range and re-simulate, similar to the existing MW adjustment for
curtailment and load shedding actions.

Backend:
- Add target_tap parameter to ManualActionRequest and simulate_manual_action
- Add _compute_pst_details() returning tap_position, low_tap, high_tap per PST
- Include pst_details in analysis enrichment and manual simulation responses

Frontend:
- Add PstDetail type and wire pst_details through API, types, and session utils
- Add purple tap adjustment UI (input + Re-simulate) in ActionFeed action cards
- Mirror all changes in standalone_interface.html

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
For PST action types in the scored actions table:
- Replace "MW Start" header with "Tap Start" showing current tap position
  and valid range [low..high] from get_pst_tap_info
- Add "Target Tap" editable column synchronized with action card cardEditTap
- Row clicks trigger handleResimulateTap for computed PST actions with
  a valid target, or pass targetTap to handleAddAction for new simulations

Backend:
- Add _get_pst_tap_start() returning {pst_name, tap, low_tap, high_tap}
- Add tap_start dict to action scores for PST types in _compute_mw_start_for_scores

Frontend:
- Add tap_start to AnalysisResult action_scores type
- Extend handleAddAction with optional targetTap parameter
- Add isPstType / hasEditableColumn logic to score table rendering
- Mirror all changes in standalone_interface.html

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
- Fix "Tap Start" showing N/A by falling back to pst_details from
  computed actions when tap_start scores are not yet available
- Fix "Target Tap" not syncing from action card to score table by
  defaulting the input value to tapInfo.tap instead of empty string
- Fix default suggested tap for uncomputed actions by using effectiveTap
  variable that falls back to the start tap position

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
_get_pst_tap_start now returns the current tap position from the base
network (via get_pst_tap_info) instead of the action's target tap value.
Frontend score table prioritizes tapStartMap (N-state) over computedPst
(post-simulation) for the Tap Start display.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
_get_pst_tap_start now reads the tap position directly from the
pypowsybl base network's N-state variant via get_phase_tap_changers(),
ensuring the original tap value is returned regardless of any
simulation that may have modified the working variant. Falls back to
the simulation environment's get_pst_tap_info only as a last resort.

Frontend score table prioritizes tapStartMap (N-state from scores)
for Tap Start display, using computedPst only as a fallback.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
Three-tier lookup for the N-state PST tap position:
1. Action's "parameters" -> "previous tap" field (from action JSON file)
2. _initial_pst_taps cache captured at network load time via
   get_phase_tap_changers() before any simulation modifies state
3. Simulation environment get_pst_tap_info as last resort

This ensures Tap Start always shows the original base network tap,
not the action's target tap or post-simulation value.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
The "previous tap" value in action_scores[type].params[actionId] is the
true N-state tap from the recommender library. Use it as the primary
source for the Tap Start column, falling back to tapStartMap and
computedPst only when not available.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
Test suite (16 tests):
- Verifies Tap Start reads from params "previous tap" (N-state)
- Verifies N/A is not shown when params exist for unsimulated actions
- Verifies Tap Start stays stable after simulation/re-simulation
- Verifies Target Tap defaults to previous tap value
- Verifies fallback chain: params > tap_start > computedPst > N/A
- Tests key variants: "previous tap", "previous_tap", "previousTap"
- Tests re-simulation API call with target_tap parameter

Robust key lookup: tries "previous tap", "previous_tap", "previousTap",
and falls back to fuzzy key matching for any key containing both
"previous" and "tap".

Debug console.log added to help diagnose actual params structure.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
Target Tap in score table now defaults to the simulated tap from
pst_details (e.g. 29) for computed actions, falling back to start
tap (previous tap) only for unsimulated actions. Priority chain:
user edit > simulated tap > start tap.

Info bubble's selected_pst_tap field now reflects the current
effective tap value instead of the static backend value.

New tests (5):
- Target Tap defaults to simulated tap when action is computed
- Target Tap defaults to start tap when action is NOT simulated
- Target Tap updates after re-simulation with a different tap
- Tap Start stays stable at 27 while Target Tap shows 29
- User editing Target Tap overrides the simulated default

Removed debug console.log statements.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
After a PST tap or MW re-simulation, all combined pairs containing the
re-simulated action now have their estimations refreshed via
computeSuperposition. This keeps the Computed Pairs tab in the Combined
Actions modal consistent with the latest simulation state.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
Temporary diagnostics to trace why combined pair estimations
aren't updating after PST tap re-simulation.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
…tion

_identify_action_elements from the library returns empty for PST tap
actions because they don't change topology (no line/bus switches).
Add a fallback that identifies the PST transformer's line index from
the action content's pst_tap field, allowing superposition estimation
to work correctly for PST actions after re-simulation.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
…sition

The library's compute_combined_pair_superposition detects PST actions
as "No-op" because PST tap changes don't produce topology changes
(no line/bus switches). When this no-op error occurs for a pair
involving a PST action, fall back to additive superposition
(betas=[1,1]) using the stored observations directly. This allows
combined pair estimations to update after PST tap re-simulation.

The additive formula: rho_combined = obs_act1 + obs_act2 - obs_start
is a first-order approximation that captures the independent effects
of both actions.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
When simulate_manual_action re-simulates an action (e.g. after a PST
tap change), selectively merge updated fields (observation, action,
action_topology, content) into the existing _dict_action entry instead
of replacing it entirely. This preserves the original library-format
structure that _identify_action_elements needs to identify PST
transformer elements for superposition estimation.

Also removes the incorrect additive superposition fallback (betas=[1,1])
that was added as a workaround — the library's compute_combined_pair_
superposition handles PST actions correctly when the entry structure
is intact.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
…al_action

Adds detailed logging to diagnose why compute_combined_pair_superposition
returns "No-op" for PST actions after re-simulation:
- _dict_action entry structure (keys, content, pst_tap)
- _identify_action_elements results
- rho and p_or deltas at PST line indexes (obs_start vs obs_act)
- Library function inputs and result
- simulate_manual_action merge diagnostics

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
The library's compute_combined_pair_superposition has act1_is_pst and
act2_is_pst parameters that control no-op detection. Without these flags
(defaulting to False), PST tap actions are checked for line-status
changes which never occur for PST actions — resulting in false "No-op"
errors. With the flags set to True, the library correctly checks for
power flow changes (abs(p_or delta) > 0.1 MW) instead.

Detect PST actions using the same logic the library uses internally:
action_type == "pst"/"pst_tap" or "pst_tap"/"pst_" in action_id.

Also fixes pre-existing test failures in test_superposition_service.py
by providing proper numpy-based mock observations and real config values
instead of MagicMock objects that caused numpy comparison errors.

https://claude.ai/code/session_01DuZKBaZ2wDnQFMd9bY3QWC
The compute_superposition estimation was excluding pre-existing overloaded
lines (like .BIESL61PRAGN) from the eligible_mask when they weren't
worsened beyond the threshold. This caused the estimated max_rho_line to
differ from simulate_manual_action, which force-includes
lines_overloaded_ids in its care_mask.

Changes:
- Force-include lines_overloaded_ids in eligible_mask (matching
  simulate_manual_action behavior)
- Prefer _analysis_context["lines_overloaded"] for lines_overloaded_ids
  (consistent with simulate_manual_action priority)
- Add diagnostic logging for eligible line counts and max_rho result

https://claude.ai/code/session_01RVqqPvcxFpaqdYprGKygdX
The previous fix force-included unfiltered lines_overloaded_ids (any line
with rho >= monitoring_factor) into the eligible_mask, bypassing the
branches_with_limits filter. Lines without permanent thermal limits (e.g.
CIVAUY712) produced wildly inaccurate rho estimates (441%).

Changes:
- Move lines_overloaded_ids computation after lines_we_care_about and
  branches_with_limits are available
- Fallback now filters by both sets + pre-existing exclusion (matching
  simulate_manual_action's vectorized overload detection)
- Restructure care_mask to match simulate_manual_action exactly:
  care + limits -> rho_combined -> pre-existing exclusion -> force-include
- Use vectorized np.isin for limits_mask instead of Python loop
- Reuse name_line_list/num_lines instead of recomputing

https://claude.ai/code/session_01RVqqPvcxFpaqdYprGKygdX
…vs simulation

Adds TOP 5 lines logging and .BIESL61PRAGN-specific checks in both
compute_superposition (estimation) and simulate_manual_action (simulation)
to identify why the estimated max line differs from the simulated max line.

Logs show: in_care_mask, in_limits, in_lwca, estimated rho, N-1 rho, N rho

https://claude.ai/code/session_01RVqqPvcxFpaqdYprGKygdX
Tests (11 new in test_superposition_monitoring_consistency.py):
- Lines without thermal limits excluded from max_rho (CIVAUY712 regression)
- Fallback lines_overloaded_ids filtered by care + limits
- N-1 overloaded lines force-included despite pre-existing N-state overloads
- Analysis context priority over recomputation
- Pre-existing overload exclusion/inclusion based on worsening
- Global max scan without caching
- PST + switching monitoring consistency
- is_rho_reduction on overloaded lines
- Monitoring factor scaling

Docs (curtailment-actions.md → curtailment-loadshedding-pst-actions.md):
- Added sections 7-10: PST actions, PST in superposition, monitoring
  consistency, superposition accuracy limitations
- Documented is_pst flag, element identification fallback, tap
  re-simulation, combined pair refresh flow
- Added real-data example showing estimation vs simulation discrepancy

https://claude.ai/code/session_01RVqqPvcxFpaqdYprGKygdX
Add diagnostic logging and improve line filtering logic in superposition
@marota
marota merged commit 1033fa2 into main Apr 11, 2026
2 checks passed
marota pushed a commit that referenced this pull request Apr 14, 2026
Session reload no longer loses data introduced by PRs #73/#78/#83/#88:

- handleRestoreSession now restores lines_overloaded_after,
  load_shedding_details, curtailment_details and pst_details on each
  ActionDetail. Previously these were dropped on reload, so the PST /
  load-shedding / curtailment editor cards rendered empty and the
  Remedial Action tab lost its post-action overload halos until the
  user re-ran analysis.
- buildSessionResult persists the sticky-header rho arrays
  (n_overloads_rho / n1_overloads_rho) alongside the overload name
  lists, guarded on matching length so misaligned legacy data is
  omitted instead of saved.
- committedNetworkPathRef is now updated on session restore so the
  "Change Network?" confirmation dialog no longer misfires (or
  silently drops the study) after a reload.

Interaction logging now captures every user gesture the replay
contract needs to faithfully reproduce a session:

- config_loaded and settings_applied include the full settings
  payload (all paths, every recommender threshold including
  min_load_shedding and min_renewable_curtailment_actions, monitoring,
  pre-existing overload threshold, ignore_reconnections,
  pypowsybl_fast_mode).
- settings_tab_changed emits { from_tab, to_tab } and skips no-op
  clicks on the already-active tab.
- New event types action_mw_resimulated and pst_tap_resimulated are
  logged from ActionFeed.handleResimulate / handleResimulateTap with
  the raw user-entered target_mw / target_tap. useActions no longer
  logs manual_action_simulated from handleActionResimulated, which
  conflated the two flows and made replay impossible.

docs/interaction-logging.md is rewritten to reflect all of the above:
documented tab_detached / tab_reattached / tab_tied / tab_untied
visualisation events (previously in types.ts but undocumented),
corrected the details shape for view_mode_changed / asset_clicked /
inspect_query_changed / sld_overlay_* / session_* to match the actual
emitted payloads, documented the applySettings / loadStudy /
changeNetwork cases on contingency_confirmed, and added a new
"Session reload fidelity" section listing exactly which fields are
persisted / restored and which are intentionally ephemeral.

Tests: 695 frontend tests still pass; sessionUtils.test.ts gains
coverage for rho persistence guards and useActions.test.ts now
asserts that handleActionResimulated does not log from the hook.

https://claude.ai/code/session_013qJjLFQWMR91ZfPTRCLFiu
marota pushed a commit that referenced this pull request Apr 14, 2026
Adds 20 new tests across three files, guarding every fix from the
previous commit.

frontend/src/hooks/useSession.test.ts (+10 tests)
  New describe block "handleRestoreSession" with a reusable
  makeCtx() / makeSession() fixture pair. Covers:
  - Full configuration restore (every field, including new
    min_load_shedding / min_renewable_curtailment_actions
    thresholds from PRs #73 / #78).
  - Legacy session fallback to 0.0 when the two new thresholds
    are absent from older JSON dumps.
  - committedNetworkPathRef.current update on success — the
    regression for the "Change Network?" dialog misfire fix.
  - api.updateConfig payload shape, including the new thresholds.
  - session_reloaded interaction event emission.
  - Empty outputFolderPath short-circuit (no API call, ref
    untouched).
  - Backend error surfacing via ctx.setError with no ref mutation.
  - Enrichment field round-trip: captures the setResult updater
    via a captureRestoredResult() helper and asserts
    load_shedding_details, curtailment_details, pst_details and
    lines_overloaded_after all land on the restored ActionDetail.
  - Action status flag restoration into selected / suggested /
    rejected / manually-simulated sets.
  - Legacy action shape (enrichment fields absent) doesn't crash.
  - Estimation-only combined entries are filtered out of top-level
    actions but survive under combined_actions.

frontend/src/components/ActionFeed.test.tsx (+4 tests)
  New "Re-simulation interaction logging" describe block:
  - action_mw_resimulated is recorded with target_mw equal to
    parseFloat(user input) when re-simulating a load-shedding
    card.
  - manual_action_simulated is NOT emitted on LS re-simulation
    (regression guard for the mistyped event in useActions).
  - pst_tap_resimulated is recorded with target_tap equal to the
    user-entered integer on PST re-simulation, and neither
    manual_action_simulated nor action_mw_resimulated leak into
    the log.
  - target_mw is parsed as a float even when the user types
    trailing zeros (5.400 → 5.4).

frontend/src/components/modals/SettingsModal.test.tsx (+5 tests)
  New "settings_tab_changed interaction log shape" describe block:
  - paths → recommender logs { from_tab: 'paths',
    to_tab: 'recommender' }.
  - paths → configurations logs the matching transition.
  - from_tab tracks the currently-active tab, not the initial one
    (rendering the modal already on 'recommender' and clicking
    'configurations' yields from_tab: 'recommender').
  - Clicking the already-active tab does NOT log a
    settings_tab_changed entry (no-op skip).
  - setSettingsTab is still called unconditionally on no-op
    clicks — pins the "setter always, logger only on transition"
    split behaviour.

Results: 39 test files, 715 tests passing (was 695). tsc -b and
eslint both clean.

https://claude.ai/code/session_013qJjLFQWMR91ZfPTRCLFiu
marota pushed a commit that referenced this pull request Apr 30, 2026
Reconciliation of section 2 (0.5.0):
- Drop misattributed PRs that were actually pre-rebrand: save/reload
  (#49/#52), MW Start (#62), interaction-logging (#64), SLD highlights
  (#63), load shedding initial integration (#61). All now properly
  cited in section 1.5–1.6.
- Disambiguate App.tsx refactor history: PR #56 (hooks, 2100 → 800,
  pre-rebrand) vs PR #74 (components, 1000 → 650, 0.5.0) vs PR #75
  (memoization Phase 2, same LoC).
- Add accurate 0.5.0 PRs: #66 (vectorization w/ benchmark table),
  #69/#70/#71 (UI polish), #72 (curtailment), #73 (loads_p/gens_p
  format + configurable MW), #74/#75 (App.tsx decomposition),
  #78 (PST tap re-simulation), #84/#86/#87/#90 (detachable tabs).
- Add a recap table summarizing what's truly new in 0.5.0.

Diagrams added (Mermaid, GitHub-rendered):
- Gantt timeline of all 4 phases (top of doc).
- High-level architecture (frontend / backend / data).
- Two-step analysis sequence diagram (section 1.6).
- App.tsx LoC evolution flow (section 2.4).
- Backend mixin decomposition before/after PR #104/#106 (section 3).
- PyPSA-EUR pipeline flowchart (section 4).

https://claude.ai/code/session_01Pg7fuCUG2edfm5PyHS6SbN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants