Waveform Post-Processing¶
Pulsim provides a backend-owned waveform post-processing pipeline as a standard workflow layer for power-electronics simulations. This page documents the configuration contract, metric definitions (with formulas), frontend consumption rules, and migration guidance.
Overview¶
Post-processing jobs are declared inside the simulation.post_processing block of a YAML netlist, or constructed programmatically via the Python API. Jobs execute after the transient simulation completes and consume the virtual_channels surface of SimulationResult.
Three job families are supported:
| Kind | YAML value | Purpose |
|---|---|---|
| Time-domain | time_domain |
RMS, mean, min, max, p2p, crest, ripple, std |
| Spectral | spectral |
FFT bins, harmonic table, THD |
| Power/efficiency | power_efficiency |
Average power, efficiency, power factor |
YAML Configuration Contract¶
simulation:
tstart: 0.0
tstop: 5e-3
dt: 1e-7
post_processing:
jobs:
- id: output_voltage_metrics
kind: time_domain
signals: [V(out)]
window:
mode: time
t_start: 3e-3
t_end: 5e-3
metrics: [rms, mean, min, max, p2p, ripple]
- id: output_spectrum
kind: spectral
signals: [V(out)]
window:
mode: time
t_start: 3e-3
t_end: 5e-3
n_harmonics: 7
window_function: hann
- id: converter_efficiency
kind: power_efficiency
input_voltage: V(Vin)
input_current: I(Vin)
output_voltage: V(out)
output_current: I(Rload)
window:
mode: time
t_start: 3e-3
t_end: 5e-3
Job Fields¶
Common to all jobs¶
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | No | Stable job identifier (auto-generated as job_N if omitted) |
kind |
string | Yes | time_domain, spectral, or power_efficiency (aliases: power, efficiency) |
window |
map | No | Window spec (default: full simulation time range) |
time_domain specific¶
| Field | Type | Default | Description |
|---|---|---|---|
signals |
list[string] or string | — | Signal names from virtual_channels (comma-separated string also accepted) |
metrics |
list[string] or string | all | Metrics to compute (see Time-Domain Metrics) |
spectral specific¶
| Field | Type | Default | Description |
|---|---|---|---|
signals |
list[string] | — | Signal names |
fundamental_hz |
float | auto-detect | Explicit fundamental frequency; auto-detected from spectrum peak if omitted |
n_harmonics |
int | 5 | Number of harmonics for THD and harmonic table |
window_function |
string | rectangular |
Window function: rectangular, hann, hamming, blackman, flat_top |
power_efficiency specific¶
| Field | Type | Description |
|---|---|---|
input_voltage |
string | Signal name for input voltage (e.g. V(Vin)) |
input_current |
string | Signal name for input current (e.g. I(Vin)) |
output_voltage |
string | Signal name for output voltage |
output_current |
string | Signal name for output current |
Window Specification¶
| Mode | Fields | Description |
|---|---|---|
time |
t_start, t_end |
Physical time bounds (seconds) |
index |
i_start, i_end, optional min_samples |
Sample index bounds (half-open: [i_start, i_end)) |
cycle |
cycle_start, cycle_end, period |
Cycle-number bounds with known switching period |
Time-Domain Metrics¶
All time-domain metrics operate on a windowed sample array $x[0], x[1], \ldots, x[N-1]$.
RMS¶
$$\text{RMS} = \sqrt{\frac{1}{N} \sum_{k=0}^{N-1} x[k]^2}$$
Power-engineering convention: includes DC component.
Mean¶
$$\bar{x} = \frac{1}{N} \sum_{k=0}^{N-1} x[k]$$
Min / Max¶
$$x_{\min} = \min_k x[k], \quad x_{\max} = \max_k x[k]$$
Peak-to-Peak (p2p)¶
$$\text{p2p} = x_{\max} - x_{\min}$$
Crest Factor¶
$$\text{CF} = \frac{\max_k |x[k]|}{\text{RMS}}$$
Undefined when $\text{RMS} = 0$ (or negligibly small relative to signal amplitude). Reported as PostProcessingDiagnosticCode.UndefinedMetric.
Ripple¶
$$\text{Ripple} = \frac{\text{p2p}}{|\bar{x}|}$$
Power-electronics convention (dimensionless). Undefined when $|\bar{x}|$ is less than 0.1% of RMS — occurs for zero-offset AC signals.
Standard Deviation¶
$$\sigma = \sqrt{\frac{1}{N} \sum_{k=0}^{N-1} (x[k] - \bar{x})^2}$$
Spectral Metrics¶
Spectral analysis uses numpy's one-sided real FFT (numpy.fft.rfft) with coherent-gain window normalization.
Window Functions¶
| Name | YAML value | Formula |
|---|---|---|
| Rectangular | rectangular |
$w[n] = 1$ |
| Hann | hann |
$w[n] = 0.5\left(1 - \cos!\left(\tfrac{2\pi n}{N-1}\right)\right)$ |
| Hamming | hamming |
$w[n] = 0.54 - 0.46\cos!\left(\tfrac{2\pi n}{N-1}\right)$ |
| Blackman | blackman |
$w[n] = 0.42 - 0.5\cos!\left(\tfrac{2\pi n}{N-1}\right) + 0.08\cos!\left(\tfrac{4\pi n}{N-1}\right)$ |
| Flat-top | flat_top |
(numpy flattop definition) |
Amplitude Spectrum¶
After windowing and normalization, each one-sided bin $k$ has amplitude:
$$A[k] = \frac{2 |X[k]|}{N \cdot \text{CG}}$$
where $X = \text{rfft}(w \cdot x)$ and $\text{CG} = \frac{1}{N}\sum_{n=0}^{N-1} w[n]$ is the coherent gain of the window.
Fundamental Detection¶
If fundamental_hz is not specified, the fundamental is taken as the frequency of the peak amplitude bin in the one-sided spectrum.
Total Harmonic Distortion (THD)¶
$$\text{THD} = \frac{\sqrt{\sum_{h=2}^{H} A_h^2}}{A_1} \times 100\%$$
where $A_1$ is the fundamental amplitude and $A_h$ is the amplitude of the $h$-th harmonic.
Undefined when $A_1 \approx 0$ (e.g. near-DC signal or fundamental not present in window). Reported as PostProcessingDiagnosticCode.UndefinedMetric.
Power and Efficiency Metrics¶
Average Power¶
$$P_{\text{avg}} = \frac{1}{N} \sum_{k=0}^{N-1} v[k] \cdot i[k]$$
For input and output:
$$P_{\text{in}} = \overline{v_{\text{in}} \cdot i_{\text{in}}}, \quad P_{\text{out}} = \overline{v_{\text{out}} \cdot i_{\text{out}}}$$
Efficiency¶
$$\eta = \frac{P_{\text{out}}}{P_{\text{in}}} \times 100\%$$
When both $P_{\text{in}} = 0$ and $P_{\text{out}} = 0$, efficiency is reported as 100% (no-load convention). When $P_{\text{in}} = 0$ and $P_{\text{out}} \neq 0$, efficiency is undefined.
Power Factor¶
$$\text{PF} = \frac{P_{\text{avg}}}{V_{\text{rms}} \cdot I_{\text{rms}}}$$
Computed from input signals only. Ranges in $[-1, 1]$; positive for lagging loads. Undefined when $V_{\text{rms}} \cdot I_{\text{rms}} = 0$.
Diagnostic Codes¶
Every job result carries a diagnostic field with a machine-stable code:
| Code | Meaning |
|---|---|
ok |
Job succeeded with no issues |
invalid_configuration |
Malformed job, missing required field, or unknown metric name |
signal_not_found |
Named signal not present in virtual_channels |
invalid_window |
Window bounds empty, reversed, or beyond simulation time |
insufficient_samples |
Window contains fewer samples than min_samples (default: 4) |
sampling_mismatch |
Signal sampling incompatible with spectral requirements |
undefined_metric |
Metric denominator is zero (crest: RMS=0; ripple: mean≈0; THD: fundamental=0) |
numerical_failure |
Unexpected numerical error during computation |
Python API¶
Configuration¶
import pulsim as ps
opts = ps.PostProcessingOptions(jobs=[
ps.PostProcessingJob(
job_id="voltage_ripple",
kind=ps.PostProcessingJobKind.TimeDomain,
signals=["V(out)"],
metrics=["rms", "mean", "ripple"],
window=ps.PostProcessingWindowSpec(
mode=ps.PostProcessingWindowMode.Time,
t_start=3e-3,
t_end=5e-3,
),
),
ps.PostProcessingJob(
job_id="spectrum",
kind=ps.PostProcessingJobKind.Spectral,
signals=["V(out)"],
n_harmonics=7,
window_function=ps.WindowFunction.Hann,
),
ps.PostProcessingJob(
job_id="efficiency",
kind=ps.PostProcessingJobKind.PowerEfficiency,
input_voltage_signal="V(Vin)",
input_current_signal="I(Vin)",
output_voltage_signal="V(out)",
output_current_signal="I(Rload)",
),
])
Execution¶
result = sim.run_transient(circuit.initial_state())
pp = ps.run_post_processing(result, opts)
if not pp.success:
for jr in pp.jobs:
if not jr.success:
print(f"[{jr.job_id}] FAILED: {jr.diagnostic} — {jr.diagnostic_message}")
Consuming Time-Domain Results¶
jr = pp.jobs[0]
if jr.success:
print("RMS: ", jr.scalar_metrics["rms"].value, jr.scalar_metrics["rms"].unit)
print("Mean: ", jr.scalar_metrics["mean"].value)
print("Ripple:", jr.scalar_metrics["ripple"].value)
for undef in jr.undefined_metrics:
print(f"Undefined: {undef.name} — {undef.reason}: {undef.message}")
Consuming Spectral Results¶
jr = pp.jobs[1]
if jr.success:
print("Fundamental:", jr.fundamental_hz, "Hz")
print("THD: ", jr.thd_pct, "%")
for h in jr.harmonics: # HarmonicEntry(harmonic_number, frequency_hz, magnitude, ...)
print(f" H{h.harmonic_number}: {h.frequency_hz:.1f} Hz mag={h.magnitude:.4f}"
f" ({h.magnitude_pct_fundamental:.2f}% of fundamental)")
Consuming Power/Efficiency Results¶
jr = pp.jobs[2]
if jr.success:
print("P_in: ", jr.average_input_power, "W")
print("P_out: ", jr.average_output_power, "W")
print("Efficiency: ", jr.efficiency, "%")
print("Power factor:", jr.power_factor)
Parsing YAML from Python¶
yaml_node = {
"jobs": [
{"id": "vout", "kind": "time_domain", "signals": ["V(out)"],
"metrics": ["rms", "ripple"]}
]
}
errors = []
opts = ps.parse_post_processing_yaml(yaml_node, errors)
if errors:
print("Parse errors:", errors)
Frontend Consumption Rules¶
The backend post-processing pipeline is the single source of truth for waveform metrics. Frontend code must not:
- Recompute RMS, THD, efficiency, or other metrics from raw
virtual_channelsdata. - Assume a specific unit or domain for a metric — always read
ScalarMetric.unitandScalarMetric.domain. - Silently ignore
undefined_metricsordiagnosticfields; surface them to the user. - Depend on free-form log output to extract metric values; always consume
scalar_metrics,harmonics,spectrum_bins,efficiency, and related structured fields.
The backend guarantees:
- Stable, ordered job results (same order as input
PostProcessingOptions.jobs). - Machine-stable diagnostic codes for programmatic branching.
- Deterministic outputs across identical repeated runs.
Migration from Script-Based Post-Processing¶
If you previously performed waveform analysis via external notebooks or scripts:
Before (ad-hoc)¶
# External notebook / script — ad-hoc approach
import numpy as np
result = sim.run_transient(circuit.initial_state())
v = np.array(result.virtual_channels["V(out)"])
t = np.array(result.time)
# Window selection was manual and error-prone
mask = (t >= 3e-3) & (t <= 5e-3)
rms = np.sqrt(np.mean(v[mask] ** 2))
thd = compute_thd_script(v[mask], t[mask], f0=20000) # custom, unvalidated
After (canonical backend)¶
import pulsim as ps
# Declare once, reuse across all runs
pp_opts = ps.PostProcessingOptions(jobs=[
ps.PostProcessingJob(
job_id="vout",
kind=ps.PostProcessingJobKind.TimeDomain,
signals=["V(out)"],
metrics=["rms"],
window=ps.PostProcessingWindowSpec(
mode=ps.PostProcessingWindowMode.Time,
t_start=3e-3, t_end=5e-3,
),
),
ps.PostProcessingJob(
job_id="spectrum",
kind=ps.PostProcessingJobKind.Spectral,
signals=["V(out)"],
fundamental_hz=20000.0,
n_harmonics=7,
window_function=ps.WindowFunction.Hann,
window=ps.PostProcessingWindowSpec(
mode=ps.PostProcessingWindowMode.Time,
t_start=3e-3, t_end=5e-3,
),
),
])
result = sim.run_transient(circuit.initial_state())
pp = ps.run_post_processing(result, pp_opts)
rms = pp.jobs[0].scalar_metrics["rms"].value
thd = pp.jobs[1].thd_pct
Migration Checklist¶
- [ ] Replace all
np.sqrt(np.mean(v**2))patterns →PostProcessingJobKind.TimeDomainwithmetrics=["rms"]. - [ ] Replace all manual FFT + harmonic scripts →
PostProcessingJobKind.Spectral. - [ ] Replace all
P_out / P_in * 100efficiency scripts →PostProcessingJobKind.PowerEfficiency. - [ ] Replace manual window slicing with
PostProcessingWindowSpec(Time/Index/Cycle). - [ ] Update frontend code to consume
scalar_metrics,thd_pct,efficiencydirectly fromPostProcessingJobResult. - [ ] Remove custom metric helper functions that duplicate backend computations.
Known Limitations and Deferred Items¶
- Settling-time and overshoot metrics: Not supported in this release. Planned for a future post-processing phase.
- Loop/stability metrics (gain margin, phase crossover): Only supported via
run_harmonic_balance/FrequencyAnalysisResult. Time-domain-derived loop metric estimators are deferred. - Cycle-window for variable-frequency switching: The
cyclewindow mode assumes a fixedperiod. Variable-frequency (e.g. spread-spectrum) is not supported. - Streaming / incremental post-processing: Jobs execute on the complete
SimulationResult. Streaming/online metric computation is deferred.