diff --git a/GridKit/Model/PhasorDynamics/Branch/README.md b/GridKit/Model/PhasorDynamics/Branch/README.md
index 2f6ed7e1a..4b1608e7e 100644
--- a/GridKit/Model/PhasorDynamics/Branch/README.md
+++ b/GridKit/Model/PhasorDynamics/Branch/README.md
@@ -20,10 +20,10 @@ provides more flexibility for modeling.
Symbol | Units | Description | Note
------------|---------|---------------------------------| ------
-$R$ | [p.u.] | Branch series resistance |
-$X$ | [p.u.] | Branch series reactance |
-$G$ | [p.u.] | Branch shunt conductance |
-$B$ | [p.u.] | Branch shunt susceptance |
+$R$ | [p.u.] | Branch series resistance |
+$X$ | [p.u.] | Branch series reactance |
+$G$ | [p.u.] | Branch shunt conductance |
+$B$ | [p.u.] | Branch shunt susceptance |
### Model Derived Parameters
Note the difference between little-g and big-G, little-b, big-B in these equations.
@@ -81,10 +81,43 @@ None.
\end{aligned}
```
-# Model Outputs
+## Actions
+
+This component accepts the following runtime events via `apply(Action)`. See
+[EVENTS.md](../EVENTS.md) for the dispatch model and JSON schema.
+
+JSON `"action"` | Params | Effect
+----------------|---------------------|----------------------------------------------------------------------
+`"open"` | none | Sets line status $S = 0$ (line tripped).
+`"close"` | none | Sets line status $S = 1$ (line in service).
+`"fault"` | `R`, `X`, `percent` | Stores the fault impedance and fractional position $f = \text{percent}/100$ along the line from `bus1`, and engages the fault ($U = 1$).
+`"clear"` | none | Disengages the fault ($U = 0$). Stored impedance is unchanged.
+
+The line status $S$ is implemented as a mask (0 or 1) on every
+branch residual contribution, so the Jacobian sparsity pattern is fixed
+across `Open`/`Close` cycles.
+
+While the fault is engaged, the line is split at the fault point into two
+series segments with admittances
+
+``` math
+y_{1m} = \dfrac{g + jb}{f}, \quad y_{m2} = \dfrac{g + jb}{1 - f}
+```
+
+connected by a shunt fault admittance $y_f = 1/(R_f + jX_f)$ to ground at
+the fault point, where $g$, $b$ are the unfaulted line series admittance
+parameters and $R_f$, $X_f$ are the fault impedance from the action payload.
+The Norton-equivalent two-port admittance of this augmented network replaces
+the unfaulted line series contribution to the residuals at `bus1` and `bus2`.
+The line charging admittances $G/2$, $B/2$ at both terminals are unchanged.
+
+The fault contribution is itself multiplied by the line status $S$: an open
+line carries no fault current regardless of $U$.
+
+## Model Outputs
Real and imaginary current at the branch's two buses
-are model variables of the branch model: $I_{r1}$, $I_{i1}$, $I_{r2}$,
+are model variables of the branch model: $I_{r1}$, $I_{i1}$, $I_{r2}$,
and $I_{i2}$.
Current is oriented leaving the branch (i.e. entering the bus).
@@ -96,7 +129,7 @@ Current magnitude $I_{m1}$ and $I_{m2}$ are the phasor magnitude of the current.
\end{aligned}
```
-Active and reactive power ($P_1$, $Q_1$, $P_2$, and $Q_2$)
+Active and reactive power ($P_1$, $Q_1$, $P_2$, and $Q_2$)
are the real and imaginary parts of the complex power at each end of the branch,
where the complex power is defined as $S=VI^{\ast}=(V_r + j V_i)(I_r - jI_i)$
``` math
@@ -112,7 +145,7 @@ Real and reactive power are oriented leaving the branch (i.e. entering the bus).
# Transformer Branch Model
-**Note: Transformer model not yet implemented**
+> **Note: Transformer model not yet implemented**
The branch model can be created by adding the ideal transformer in series with
the $`\pi`$ circuit as shown in Figure 2 where $`\tau`$ is a tap ratio
@@ -121,8 +154,8 @@ $`N = \tau e^{j \theta}`$.

-
-
+
+
Figure 2: Branch equivalent circuit
diff --git a/GridKit/Model/PhasorDynamics/Bus/README.md b/GridKit/Model/PhasorDynamics/Bus/README.md
index 2d61eed2f..c26a2986e 100644
--- a/GridKit/Model/PhasorDynamics/Bus/README.md
+++ b/GridKit/Model/PhasorDynamics/Bus/README.md
@@ -17,7 +17,7 @@ sign.

-
+
Figure 1: Needs to be changed to represent current balance instead of power
balance.
@@ -28,3 +28,27 @@ sign.
**Other Parameters**
Buses are uniquely defined by their ID (number or name). Besides, each bus
should have associated Nominal Voltage value.
+
+## Actions
+
+This component accepts the following runtime events via `apply(Action)`. See
+[EVENTS.md](../EVENTS.md) for the dispatch model and JSON schema.
+
+JSON | Params | Effect
+----------------|----------|--------------------------------------------------------------------
+`"fault"` | `R`, `X` | Stores the fault impedance and engages the fault ($U = 1$).
+`"clear"` | none | Disengages the fault ($U = 0$). Stored impedance is unchanged.
+
+While the fault is engaged, the bus current balance gains the contributions:
+
+``` math
+\begin{aligned}
+ G_f &= \dfrac{R}{R^2 + X^2}, \quad B_f = -\dfrac{X}{R^2 + X^2} \\
+ \Delta I_{rk} &= U(-G_f V_{rk} + B_f V_{ik}) \\
+ \Delta I_{ik} &= U(-B_f V_{rk} - G_f V_{ik})
+\end{aligned}
+```
+
+The `percent` parameter of `Fault` is ignored by `Bus`. The fault gate $U$
+is implemented as a mask (0 or 1) on the fault residual term,
+so the Jacobian sparsity pattern is fixed across `Fault`/`Clear` cycles.
diff --git a/GridKit/Model/PhasorDynamics/EVENTS.md b/GridKit/Model/PhasorDynamics/EVENTS.md
new file mode 100644
index 000000000..91dec8518
--- /dev/null
+++ b/GridKit/Model/PhasorDynamics/EVENTS.md
@@ -0,0 +1,115 @@
+# Phasor Dynamics Events
+
+## Overview
+
+This document describes the runtime event system for PhasorDynamics
+components. A runtime event mutates the state of a single component during simulation. Events are scheduled in a solver file
+(see [PDSim](../../../application/PhasorDynamics/README.md)) and delivered to components by `SystemModel` at their scheduled times. Buses are addressed for events by their existing `number`; devices by their
+existing `id`.
+
+## Event
+
+An event is a triple of fields:
+
+Name | Description
+---------|------------------------------------------------------
+`time` | Simulation time at which the event fires, in seconds
+`target` | Routing key for the component receiving the event
+`action` | The mutation to perform, drawn from the action vocabulary
+
+For buses, the `target` is the bus's `number` rendered as a string
+(e.g. `"1001"`). For devices, it is the device's `id` field from the case
+file (e.g. `"BR_1001_1064_1"`). Buses and devices share a single routing
+namespace; collisions between a bus number and a device id are an error at registration.
+
+
+```cpp
+struct Event {
+ double time;
+ std::string target;
+ Action action;
+};
+```
+
+## Action vocabulary
+
+`Action` is a `std::variant` of action structs in
+`GridKit::PhasorDynamics::Actions`. Each struct has a canonical name string
+exposed as `static constexpr std::string_view name`.
+
+C++ type | JSON name | Params | Applies to
+-----------------|-----------|---------------------|----------------
+`Actions::Open` | `open` | none | `Branch`
+`Actions::Close` | `close` | none | `Branch`
+`Actions::Fault` | `fault` | `R`, `X`, `percent` | `Bus`, `Branch`
+`Actions::Clear` | `clear` | none | `Bus`, `Branch`
+
+The semantics of each action are documented in the receiving component's
+README. For `fault`, the `percent` parameter (position along the line as a
+percent in `[0, 100]`) is used by `Branch` and ignored by `Bus`.
+
+## Dispatch
+
+`Component::apply(const Action&)` is virtual on the base class. The default
+throws. Components that handle events override it with a `std::visit` over
+the variant, dispatching to per-action handlers and falling through to a
+generic catch-all for unhandled action types:
+
+```cpp
+void Bus::apply(const Action& a) override {
+ std::visit(overloaded{
+ [this](const Actions::Fault& f) {},
+ [this](const Actions::Clear&) {},
+ [this](const auto& other) {
+ using A = std::decay_t;
+ throw std::runtime_error("Bus does not handle action '" + std::string(A::name) + "'");
+ }
+ }, a);
+}
+```
+
+`SystemModel::apply(const Event&)` looks up the target by string id and
+forwards the action to the component.
+
+## Schedule
+
+The `schedule` field of the solver file is an array of events. The parser
+stable-sorts the schedule by `time` on read. If the input was not already
+sorted, an info line is logged and parsing continues.
+
+Multiple events at the same time are applied sequentially in listing order in a single re-init of the integrator (`IDACalcIC`). When two same-time events conflict on a single target, the last-listed event wins.
+
+The same-time grouping uses exact `double` equality on `time`. Both sides of the comparison are read from the parsed event records, so events with the same JSON literal compare equal.
+
+## Example schedule
+
+A schedule that faults bus `2` at `t=1.0`, clears the fault at `t=1.1`, and
+simultaneously opens line `L23` at `t=1.1`:
+
+```json
+"schedule": [
+ { "time": 1.0, "target": "2", "action": "fault", "params": { "R": 0.0, "X": 1.0e-5 } },
+ { "time": 1.1, "target": "2", "action": "clear" },
+ { "time": 1.1, "target": "L23", "action": "open" }
+]
+```
+
+## Errors
+
+Failure | Source | Message
+-------------------------------|---------------------|--------
+Unknown target id | `SystemModel` | `No event target with id ''`
+Unhandled action for component | `Component::apply` | `Component '' does not handle action ''`
+Unknown action string | parser | `unknown action '' (valid: open, close, fault, clear)`
+Missing required `params` | parser | `action '' requires params field with `
+Schedule entry past `tmax` | parser | `schedule entry at t= exceeds tmax=`
+
+## Adding a new action
+
+1. Add a struct to `GridKit::PhasorDynamics::Actions` with a
+ `static constexpr std::string_view name` and any payload fields.
+2. Add the struct to the `Action` variant alias.
+3. Add a parser case mapping the JSON name to the struct.
+4. Override `apply` in the receiving component(s) to handle the new action.
+5. Add a row to the action vocabulary table above and an entry in each
+ receiving component's README Actions section.
diff --git a/application/PhasorDynamics/README.md b/application/PhasorDynamics/README.md
index c3da80264..8178146f9 100644
--- a/application/PhasorDynamics/README.md
+++ b/application/PhasorDynamics/README.md
@@ -1,25 +1,88 @@
-# Input file for GridKit phasor dynamics application
+# PDSim (Phasor Dynamics Simulation)
-## Root elements
- Name | Value
- ---------------------|-------------------------------------------------------
- `system_model_file` | Path to the system model file[^1]
- `dt` | A floating point value for time step size
- `tmax` | A floating point value for max time
- `events` | An array of event groups (see [Events](#events) below)
- `output_file` | Path to output (CSV) file
- `reference_file` | A string containing the name of the case
- `error_tolerance` | A string containing the name of the case
+PDSim runs a phasor-dynamics simulation defined by a *solver file*
+(`*.solver.json`) and a *case file* (`*.case.json`). The case file specifies
+the system model, and the solver file specifies the simulation enviornment, including
+any runtime events scheduled during the simulation.
-[^1]: See system model [case format](../../Model/PhasorDynamics/INPUT_FORMAT.md)
+The runtime event system (action vocabulary, dispatch model, schedule
+semantics) is documented in [`Model/PhasorDynamics/EVENTS.md`](../../GridKit/Model/PhasorDynamics/EVENTS.md).
-## Events
+## Usage
-Each event group describes a system event that occurs at a given time point
+```
+pdsim
+```
- Name | Value
- --------------------|-------------------------------------------------------
- `time` | A floating point value for time event occurs
- `type` | Event type (one of { "fault_on", "fault_off" })
- `element_id` | An integer value referencing the element associated with the event (e.g., bus fault id)
+Relative paths in `case_file`, `output_file`, and `reference_file` are
+resolved against the solver file's directory. Absolute paths are used as
+given.
+
+## Solver file format
+
+### Top-level fields
+
+Name | Required | Description
+------------------|----------|------------------------------------------------------
+`format_version` | no | Format version integer. Default `1`. Parser rejects unknown versions.
+`case_file` | yes | Path to the case file
+`dt` | yes | Reporting time step in seconds
+`tmax` | yes | End time in seconds
+`schedule` | no | Ordered array of events (see below). May be empty or omitted.
+`output_file` | no | Path to monitor output (CSV)
+`reference_file` | no | Path to reference CSV for validation
+`error_tolerance` | no | Maximum allowed error vs reference. Default `1e-4`.
+
+### Schedule
+
+The `schedule` field is an array of events. Each event has the shape:
+
+Name | Required | Description
+---------|--------------------|------------------------------------------------------
+`time` | yes | Simulation time at which the event fires, in seconds. Must be `≤ tmax`.
+`target` | yes | Routing key of the component receiving the event (bus `number` or device `id`)
+`action` | yes | Action name; one of `open`, `close`, `fault`, `clear`
+`params` | iff action carries | Action payload (e.g. `{ R, X, percent }` for `fault`)
+
+The action vocabulary, dispatch model, schedule canonicalization (stable
+sort by time, last-listed wins on conflicts), and same-time semantics are
+documented in
+[`EVENTS.md`](../../GridKit/Model/PhasorDynamics/EVENTS.md).
+
+## Output and validation
+
+If `output_file` is given, it sets the destination for the case file's
+default CSV monitor sink. Other monitor sinks declared in the case file
+are unaffected. Output formatting (channels, delimiter) is controlled by
+the case file's monitor declarations.
+
+If both `output_file` and `reference_file` are given, PDSim compares the
+output against the reference and exits with status `0` if the maximum
+error is within `error_tolerance`, `1` otherwise.
+
+## Example
+
+```json
+{
+ "format_version": 1,
+ "case_file": "texas.json",
+ "dt": 0.00416666666666,
+ "tmax": 20.0,
+ "schedule": [
+ { "time": 10.0, "target": "1001", "action": "fault",
+ "params": { "R": 0.0, "X": 1.0e-5 } },
+ { "time": 10.15, "target": "1001", "action": "clear" },
+ { "time": 10.15, "target": "BR_1001_1064_1", "action": "open" }
+ ],
+ "output_file": "texas.csv",
+ "reference_file": "texas.ref.csv",
+ "error_tolerance": 1.0e-4
+}
+```
+
+This run faults bus `1001` at `t=10`, clears the fault and trips line
+`BR_1001_1064_1` simultaneously at `t=10.15` (a 9-cycle fault followed by
+isolation of the faulted equipment), and integrates to `t=20`. The two
+events at `t=10.15` batch into one IDA re-init. Output is compared against
+the reference file with tolerance `1e-4`.