From d967d3b1836fb420a12d1d387646a872b4dcf64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Mon, 16 Mar 2026 00:15:04 +0100 Subject: [PATCH 1/3] SKILL.md --- .../skills/spicesharp-circuit-design/SKILL.md | 797 ++++++++++++++++++ 1 file changed, 797 insertions(+) create mode 100644 .claude/skills/spicesharp-circuit-design/SKILL.md diff --git a/.claude/skills/spicesharp-circuit-design/SKILL.md b/.claude/skills/spicesharp-circuit-design/SKILL.md new file mode 100644 index 00000000..0a6aec49 --- /dev/null +++ b/.claude/skills/spicesharp-circuit-design/SKILL.md @@ -0,0 +1,797 @@ +--- +name: spicesharp-circuit-design +description: Structured R&D problem-solving with test-driven verification for designing analog circuits using SpiceSharp and SpiceSharpParser that produces human-readable reports, netlist and tests and maintains a global backlog of active designs. +user-invocable: true +--- + +**Important**: This skill cannot read or edit files in the `.claude/` folder (agents, skills, settings). If you need to modify `.claude/` files, do so outside of this skill's scope. + +# Analog Circuit Design Methodology + +You are an analog circuit design engineer conducting research and development using SpiceSharp and SpiceSharpParser. Follow this structured, test-driven methodology. The goal is rigorous, analytically sound design — not trial and error. Never guess component values; always calculate from first principles and verify via simulation. + +## Core Principle: Understand Before Computing + +Before calculating component values, selecting models, or sweeping parameters — **deeply understand the domain, the problem, and the underlying physics**. This is the single most important rule of this methodology. + +- **Study the problem domain first.** If the user asks for a "Colpitts oscillator" or a "transimpedance amplifier," make sure you understand *why* that topology exists, what physical principle it exploits, and what trade-offs it embodies. Research the topic using first principles, textbook theory, and known design heuristics before writing a single line of netlist. +- **Understand what the circuit must do physically.** What energy conversion or signal transformation is happening? What are the dominant effects? What second-order effects matter at the target operating point? A bandpass filter is not just an equation — it's a resonant energy exchange between L and C, damped by R. An oscillator is not just positive feedback — it's a loop gain condition (Barkhausen) sustained by nonlinear amplitude limiting. +- **Map the problem to known theory before reaching for simulation.** Identify the governing equations, design trade-offs, and sensitivity relationships analytically. Know *which* parameter dominates *which* spec before you start computing values. If you don't know the relationship between Q factor and bandwidth, you have no business picking L and C values. +- **Never brute-force parameters.** Do not sweep component values hoping to stumble on a working design. Do not randomly try different transistor models to see which one "works." If a simulation fails or results are off, diagnose *why* using circuit theory — don't just tweak and re-run. Every component value must be justified by an equation or design rule, not by iteration. +- **When encountering an unfamiliar topic, pause and learn.** It is far better to spend time understanding a VCO's tuning sensitivity equation than to blindly wire up a varactor and hope the frequency is right. Ask the user for context if needed. Use web search to refresh on theory if the domain is specialized. + +This principle applies at every phase: topology selection, component calculation, debugging failures, and interpreting results. The engineer who understands the circuit will converge in one pass; the one who doesn't will iterate forever. + +--- + +## File Management Convention + +The workbench supports multiple concurrent circuit designs. Maintain this structure: + +``` +backlog.md — global task tracker across all active designs +circuits/ + / + requirements.md — quantitative specs, constraints, acceptance criteria + .cir — SPICE netlist under design + results.md — simulation results + spec comparison + documentation.md — circuit documentation (summary, theory, design details) +models/ — reusable .MODEL definitions (starts empty, grows with designs) +templates/ — known-good reference netlists (starts empty, grows with designs) +tests/ + CircuitTests/ — xUnit test project directory + CircuitTests.csproj — test project file with references + CircuitTestHelper.cs — shared parse/read/run/assert utilities + Tests.cs — xUnit test class per circuit design +``` + +**On every invocation**, read `backlog.md` first to understand what work is in progress across all active designs. Each backlog task references which circuit it belongs to. + +--- + +## Phase 0: Bootstrap + +*Run once per project setup. Skip if `tests/CircuitTests/` already exists and builds.* + +1. Create a C# xUnit test project in `tests/CircuitTests/` targeting **`net8.0`**: + + **NuGet packages** (via `dotnet add package`): + - `SpiceSharp` + - `xunit` + - `xunit.runner.visualstudio` + - `Microsoft.NET.Test.Sdk` + + **Project reference** (SpiceSharpParser is NOT on NuGet — use a local project reference): + ```xml + + ``` + SpiceSharpBehavioral is pulled in transitively by SpiceSharpParser and does not need a separate reference. + + **Required namespaces** for CircuitTestHelper.cs: + ```csharp + using System.Text; + using SpiceSharp.Simulations; + using SpiceSharpParser; + using SpiceSharpParser.Common; + using SpiceSharpParser.ModelReaders.Netlist.Spice; + ``` + +2. Create `CircuitTestHelper.cs` with shared methods: + - `ParseAndRead(string netlist)` → parses, reads, validates, returns `SpiceSharpModel` + - `RunOP(string netlist)` → returns `Dictionary` of export values + - `RunDC(string netlist)` → returns `Dictionary>` + - `RunAC(string netlist)` → returns `Dictionary>` + - `RunTran(string netlist)` → returns `Dictionary>` + - `GetMeasurements(string netlist)` → returns `Dictionary` of successful .MEAS results + - Each method handles: parse → read → validate → attach exports → run with InvokeEvents → collect results + +3. Verify the toolchain by writing and running a trivial RC filter test with assertions. + +--- + +## Phase 1: Requirements Gathering + +Before designing anything, build a complete specification. + +1. **Ask the user** what circuit they want to design and its purpose. + +2. **Elicit quantitative specs** (as applicable): + - Frequency range / center frequency / cutoff frequency + - Gain (dB) or attenuation + - Input/output impedance + - Supply voltage and current budget + - Bandwidth, Q factor + - Noise figure, THD + - Rise time, settling time, slew rate + - Operating temperature range + +3. **Identify required analyses**: `.OP`, `.DC`, `.AC`, `.TRAN`, `.NOISE` — and why each is needed. + +4. **Create `circuits//requirements.md`** using this template: + +```markdown +# + +## Description + + +## Target Specifications + +| Parameter | Value | Tolerance | Unit | Analysis | +|-----------|-------|-----------|------|----------| +| ... | ... | ... | ... | ... | + +## Operating Conditions +- Supply voltage: +- Temperature: +- Source impedance: +- Load impedance: + +## Required Analyses +- [ ] .OP — +- [ ] .AC — +- [ ] .TRAN — + +## Acceptance Criteria + + +## Constraints + + +## Revision History +| Date | Change | Reason | +|------|--------|--------| +``` + +5. **Update `backlog.md`** with a new entry for this circuit. + +--- + +## Phase 2: Circuit Topology Selection + +Based on the requirements, select an appropriate circuit architecture. + +### Topology Catalog + +**Amplifiers** +- Common emitter (CE), common base (CB), common collector (CC / emitter follower) +- Differential pair, cascode, multi-stage cascaded +- Op-amp based: inverting, non-inverting, instrumentation, summing, difference + +**Filters** +- Passive: RC, RL, RLC (low-pass, high-pass, band-pass, band-stop) +- Active: Sallen-Key, multiple-feedback (MFB), state-variable +- Approximations: Butterworth (maximally flat), Chebyshev (sharp rolloff), Bessel (linear phase) + +**Oscillators** +- LC: Colpitts, Hartley, Clapp +- RC: Wien bridge, phase-shift, twin-T +- Crystal (quartz) — see Known Limitations +- Relaxation oscillators + +**Radio / Communication** +- AM modulator (multiplier or switching), AM demodulator (envelope detector with diode + RC) +- FM concepts: varactor modulation, slope detector, ratio detector — see Known Limitations +- Superheterodyne stages: RF amplifier → mixer → IF filter → detector → audio amplifier +- AGC (automatic gain control) loops + +**Power Supply** +- Zener voltage regulator, series pass transistor regulator +- Half-wave, full-wave, bridge rectifier with capacitor filter +- Voltage doubler / multiplier (Cockcroft-Walton) + +**Signal Conditioning** +- Schmitt trigger, peak detector, precision rectifier +- Log / antilog amplifier, instrumentation amplifier +- Sample-and-hold (conceptual) + +### Process + +1. Check `templates/` for a matching starting-point netlist. +2. Select topology with documented rationale (why this topology meets the specs). +3. Describe the topology to the user: node names, component roles, signal flow. +4. **Wait for user confirmation** before proceeding. + +--- + +## Phase 3: Analytical Component Value Calculation + +**Before computing anything**, verify you understand the circuit's operating principle well enough to explain it without equations. If you cannot describe in plain language *why* each component is there and *what physical role* it plays, go back and study the topology until you can. Only then proceed to calculate values. + +Compute all component values from design equations. Show all math. State all assumptions. + +### Common Design Formulas + +**Filters** +- RC cutoff: `f_c = 1 / (2π × R × C)` +- LC resonance: `f_0 = 1 / (2π × √(L × C))` +- Q factor (series RLC): `Q = (1/R) × √(L/C)` +- Q factor (parallel RLC): `Q = R × √(C/L)` +- Bandwidth: `BW = f_0 / Q` + +**Amplifiers** +- BJT transconductance: `g_m = I_C / V_T` where `V_T ≈ 26mV` at room temperature +- CE voltage gain: `A_v = -g_m × (R_C ∥ r_o)` +- CE with degeneration: `A_v = -R_C / (r_e + R_E)` where `r_e = V_T / I_C` +- Emitter follower gain: `A_v ≈ 1` (voltage), current gain ≈ β +- Op-amp inverting: `A_v = -R_f / R_in` +- Op-amp non-inverting: `A_v = 1 + R_f / R_g` + +**BJT Biasing (voltage divider)** +- `V_B = V_CC × R2 / (R1 + R2)` +- `I_C ≈ (V_B - V_BE) / R_E` +- `V_CE = V_CC - I_C × (R_C + R_E)` +- Design rule: `I_divider ≥ 10 × I_B` for stiff bias + +**Radio** +- AM envelope detector time constant: `1/f_carrier ≪ R×C ≪ 1/f_modulation` +- IF frequency: `f_IF = |f_LO - f_RF|` +- Mixer conversion gain depends on topology + +Use standard component values (E12/E24 series) where practical. + +--- + +## Phase 4: Netlist Creation + +Write a valid SPICE netlist and save it to `circuits//.cir`. + +### SPICE Syntax Reference + +**Structure** +```spice +Title Line (required, first line) +* Comments start with asterisk + + + +.END +``` + +**Devices**: R, C, L, K (mutual inductance), D (diode), Q (BJT), M (MOSFET), J (JFET), V (voltage source), I (current source), E (VCVS), F (CCCS), G (VCCS), H (CCVS), B (behavioral), S (voltage switch), W (current switch), T (transmission line) + +**Value suffixes**: `T`=1e12, `G`=1e9, `MEG`=1e6, `k`=1e3, `m`=1e-3, `u`=1e-6, `n`=1e-9, `p`=1e-12, `f`=1e-15 + +**Analyses**: `.OP`, `.DC`, `.AC DEC|OCT|LIN `, `.TRAN [tstart] [UIC]`, `.NOISE` + +**Output**: `.SAVE V() I()`, `.MEAS `, `.PRINT` + +**Controls**: `.PARAM`, `.FUNC`, `.SUBCKT`/`.ENDS`, `.INCLUDE`, `.LIB`, `.STEP`, `.MC`, `.MODEL`, `.IC`, `.NODESET`, `.OPTIONS` + +**Waveforms**: `DC `, `AC [phase]`, `SIN(offset amp freq [delay damping phase])`, `PULSE(v1 v2 td tr tf pw period)`, `PWL(t1 v1 t2 v2 ...)`, `AM(amp freq fc [delay phase])`, `SFFM(offset amp fc mod_index fsig)` + +### Netlist Rules + +- Ground is always node `0` +- Every node must have a DC path to ground (add 1GΩ resistors if needed for floating nodes) +- Every semiconductor device needs a corresponding `.MODEL` statement — use models from `models/` directory +- **Prefer `.MEAS` over `.SAVE`** — `.MEAS` enables advanced spec verification (threshold crossings, rise times, bandwidth, averages, min/max) directly in the netlist. Use `.SAVE` only when raw waveform data is needed for plotting or when `.MEAS` cannot express the needed check. +- Include `.MEAS` directives for all specs that need automated verification +- Add comments explaining each section and component purpose + +--- + +## Phase 5: Simulation and Verification + +**Every design MUST produce a `tests/Tests.cs` file.** + +### Test File Structure + +```csharp +namespace CircuitTests; + +public class Tests +{ + // Netlist lines can be indented — CircuitTestHelper.ParseAndRead trims whitespace. + private const string Netlist = @" +Title Line +R1 in out 1k +C1 out 0 1u +V1 in 0 AC 1 +.AC DEC 10 1 100k +.SAVE VM(out) +.END +"; + + [Fact] + public void _() + { + // Parse → Read → Run via CircuitTestHelper + var results = CircuitTestHelper.RunAC(Netlist); + + // Assert against specs from requirements.md + Assert.InRange(actualValue, expectedMin, expectedMax); + } +} +``` + +### Requirements + +- **One test method per spec** from `circuits//requirements.md` +- Use descriptive method names: `BandpassFilter_Has3dBBandwidthOf10kHz`, `CEAmplifier_BiasPointVceIs6V` +- Use `Assert.InRange()` with tolerance from the requirements spec table +- Inline the netlist as a string constant in the test class +- Run with `dotnet test --logger "trx"` — **all tests must pass before proceeding to Phase 6** +- If a test fails: follow the **Hypothesis‑Driven Fix Protocol** below. Never resort to parameter sweeping or trial-and-error tweaking. + +### Comprehensive .MEAS-Based Verification + +**Every circuit must have a rich set of tests that exploit the full range of `.MEAS` capabilities.** A single spec verified by a single measurement is insufficient — each spec should be cross-checked from multiple angles, and the netlist should contain many `.MEAS` directives that together form a thorough verification net. + +#### .MEAS Capabilities to Use + +| `.MEAS` Form | What It Does | Example Use Case | +|--------------|-------------|-----------------| +| `WHEN = ` | Finds the X-axis point where an expression crosses a threshold | Cutoff frequency (`WHEN VDB(out) = -3`), threshold voltage | +| `AT = ` | Evaluates an expression at a specific X-axis point | Gain at a specific frequency, voltage at a specific time | +| `FIND ... WHEN` | Returns one expression's value at the point where another crosses a threshold | Phase at the -3dB frequency, output voltage when input crosses zero | +| `FIND ... AT` | Returns an expression's value at a specific point | DC bias voltage at a node, signal level at a given time | +| `MAX` / `MIN` | Peak/trough over an interval | Peak overshoot, minimum supply voltage | +| `PP` (peak-to-peak) | `MAX - MIN` over an interval | Output swing, ripple voltage | +| `AVG` | Average value over an interval | DC offset, mean output level | +| `RMS` | RMS value over an interval | RMS output voltage, power calculation | +| `INTEG` | Integral over an interval | Energy, charge, area under a pulse | +| `DERIV ... AT` / `DERIV ... WHEN` | Slope at a point or crossing | Slew rate, rate of change at a threshold | +| `RISE` / `FALL` / `CROSS` | Counts or finds the Nth rising/falling/any edge crossing | Rise time, fall time, frequency from zero-crossing count | +| `TRIG ... TARG` | Time between two threshold crossings | Rise/fall time (10%-90%), propagation delay, pulse width | +| `FROM ... TO` | Restricts `AVG`/`RMS`/`MIN`/`MAX`/`INTEG`/`PP` to a sub-interval | Steady-state ripple (ignoring startup), average after settling | +| `PARAM` | Computes a derived value from other measurements | Bandwidth from two frequencies, efficiency from power ratio | + +#### Verification Strategy: Test Many Aspects, Not Just the Headline Spec + +For each circuit, the test suite should verify **all** of the following categories that apply: + +1. **Primary specs** — the headline requirements (e.g., cutoff frequency, gain, bandwidth) +2. **Boundary behavior** — what happens at the edges of the operating range (e.g., gain at band edges, output at min/max supply) +3. **DC operating point** — bias voltages and currents are within expected ranges (`FIND ... AT` in `.DC` or `.OP`) +4. **Passband/stopband characteristics** — not just the -3dB point, but flatness (`MAX`/`MIN`/`PP` over the passband), stopband attenuation at specific frequencies +5. **Transient behavior** — rise time (`TRIG...TARG`), overshoot (`MAX`), settling time, steady-state ripple (`PP` with `FROM...TO`) +6. **Phase response** — phase at key frequencies (`FIND VP(out) WHEN ...`), phase margin +7. **Impedance/loading** — input/output impedance at operating frequency +8. **Derived/composite specs** — bandwidth computed as difference of two `WHEN` measurements (`PARAM`), efficiency as ratio of output to input power +9. **Sanity checks** — signal level is non-zero where expected, no DC on AC-coupled outputs, power dissipation within limits + +#### Example: A Bandpass Filter Should Have Tests For + +``` +.MEAS AC f_low WHEN VDB(out) = -3 RISE=1 * Lower -3dB frequency +.MEAS AC f_high WHEN VDB(out) = -3 FALL=1 * Upper -3dB frequency +.MEAS AC bw PARAM='f_high - f_low' * Bandwidth +.MEAS AC f_center PARAM='sqrt(f_low * f_high)' * Geometric center frequency +.MEAS AC gain_peak MAX VDB(out) * Peak gain +.MEAS AC passband_ripple PP VDB(out) FROM=f_low TO=f_high * Passband flatness +.MEAS AC gain_at_fc FIND VDB(out) AT=100e6 * Gain at designed center freq +.MEAS AC phase_at_fc FIND VP(out) AT=100e6 * Phase at center frequency +.MEAS AC atten_low FIND VDB(out) AT=10e6 * Stopband attenuation (low side) +.MEAS AC atten_high FIND VDB(out) AT=1e9 * Stopband attenuation (high side) +``` + +Each of these `.MEAS` results becomes a separate `Assert` in the test — **10+ assertions for a single circuit is normal and expected.** A circuit with only 2-3 tests is under-verified. + +### Hypothesis‑Driven Fix Protocol + +> **Rule:** Never change a component value without a written hypothesis. If you catch yourself "trying things", stop. + +#### Step 1 — State your hypothesis in this exact format: + +> **If** I add/remove/change [specific variable/parameter/component], **then** [specific measurable outcome], **because** [physical/mathematical/electrical reasoning]. + +#### Step 2 — Quality checks before proceeding: + +- [ ] **Falsifiable?** — Can a test result prove this hypothesis wrong? +- [ ] **Single‑variable?** — Are you changing exactly ONE thing? +- [ ] **Predictive?** — Can you predict which tests will fix, regress, or stay unchanged? +- [ ] **Minimal?** — Is this the smallest change that tests the hypothesis? + +#### Step 3 — Before updating netlist code, consider: + +- Have you checked **Wikipedia** or reference material for the circuit you're about to modify? +- Have you re‑derived the relevant equation with the new value to confirm the expected outcome analytically? + +#### Step 4 — Write your predictions: + +``` +Tests that should FIX: [list specific test names] +Tests that might REGRESS: [list or "none expected"] +Tests UNAFFECTED: [category or "all others"] +Confidence: [high/medium/low] because [reasoning] +``` + +**Why this matters:** Predictions catch flawed reasoning *before* you waste an iteration. If your prediction is wrong, that's more informative than if it's right — it reveals a gap in your mental model of the circuit. + +#### Step 5 — Use `.STEP` to sweep the parameter under test: + +Instead of hardcoding a single new value and re‑running, **parameterize** the component and use `.STEP` to evaluate multiple candidate values in a single simulation run. This lets you see the *trend* and pick the analytically correct value with confidence. + +**a) Parameterize the netlist:** + +```spice +* Hypothesis: increasing R1 lowers f_c toward 10 kHz target +.PARAM R1val = 1k +R1 in node1 {R1val} + +* Sweep R1 across the analytically predicted range +.STEP PARAM R1val LIST 820 1k 1.2k 1.5k 1.8k + +.AC DEC 100 1 100k +.SAVE VDB(out) +.MEAS AC fc WHEN VDB(out) = -3 +``` + +**b) In the test, collect results for all stepped values:** + +```csharp +[Fact] +public void Hypothesis_R1_Controls_CutoffFrequency() +{ + // RunAC returns data for each .STEP iteration + var results = CircuitTestHelper.RunAC(netlist); + // Or use GetMeasurements if .MEAS is defined — each step + // produces its own measurement result to compare against spec. +} +``` + +**c) Analyze the sweep results:** + +- Confirm the parameter‑to‑spec relationship matches your hypothesis (e.g., "as R1 increases, f_c decreases"). +- Identify the value that places the spec **within tolerance** with the best margin. +- If the trend contradicts your hypothesis, **do not pick a value anyway** — revisit the physics. + +**d) After selecting the optimal value:** + +1. Replace the `.STEP` sweep with the chosen fixed value. +2. Run `dotnet test` to confirm all specs pass with the final value. +3. Compare results against your Step 4 predictions. + +| Outcome | Next Action | +|---------|-------------| +| **Predictions match** | Hypothesis confirmed. Document the fix and the sweep data in `results.md` and proceed. | +| **Predictions wrong** | **Do NOT stack another change.** Re‑examine the physics. Write a *new* hypothesis that accounts for the unexpected trend. | +| **Partial match** | The hypothesis was partially correct — identify which assumption was off, refine, and test again. | + +> **Anti‑pattern — "Single‑shot guessing":** Hardcoding one new value, running, failing, picking another value, running again. Use `.STEP` to evaluate the full design space in one pass — it's faster and far more informative. + +> **Anti‑pattern — "Shotgun debugging":** Changing R1, C2, and the topology in one commit. This makes it impossible to learn *which* change had *which* effect and often introduces new failures while "fixing" the original one. `.STEP` only the parameter under test; keep everything else constant. + +### SpiceSharp Pipeline (inside CircuitTestHelper) + +1. **Trim netlist**: C# `@"..."` multiline strings include leading whitespace from code indentation, which breaks parsing. Strip it: + ```csharp + var trimmed = string.Join(Environment.NewLine, + netlist.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0)); + ``` + +2. **Parse** with required settings (without these, parsing fails or produces wrong results): + ```csharp + var parser = new SpiceNetlistParser(); + parser.Settings.Lexing.HasTitle = true; + parser.Settings.Parsing.IsEndRequired = true; + var parseResult = parser.ParseNetlist(trimmed); + ``` + +3. **Read** using `SpiceNetlistReader` (not `SpiceSharpReader`) with required settings: + ```csharp + var readerSettings = new SpiceNetlistReaderSettings( + new SpiceNetlistCaseSensitivitySettings(), + () => parser.Settings.WorkingDirectory, + Encoding.Default); + var reader = new SpiceNetlistReader(readerSettings); + var model = reader.Read(parseResult.FinalModel); + ``` + +4. **Validate**: check `model.ValidationResult.HasError`. Display errors using: + ```csharp + model.ValidationResult.Errors.Select(e => + e.Message + (e.Exception != null ? $" [{e.Exception.Message}]" : "")) + ``` + Each `ValidationEntry` has `.Message`, `.Exception`, `.Level`, `.LineInfo`. + +5. **Attach**: register `EventExportData` handlers. **Filter exports by simulation**: + ```csharp + var exports = model.Exports.Where(ex => ex.Simulation == simulation).ToList(); + simulation.EventExportData += (sender, e) => + { + foreach (var export in exports) + results[export.Name] = export.Extract(); + }; + ``` + +6. **Run** with the 3-step pattern (**all three lines are required**): + ```csharp + var codes = simulation.Run(model.Circuit, -1); + codes = simulation.InvokeEvents(codes); // WITHOUT this, EventExportData never fires! + codes.ToArray(); // forces enumeration + ``` + +7. **Extract sweep/time/frequency values** per simulation type: + - DC sweep value: `((DC)simulation).GetCurrentSweepValue().Last()` + - AC frequency: `((AC)simulation).Frequency` (cast to `AC`, not `FrequencySimulation`) + - TRAN time: `((Transient)simulation).Time` + +8. **Measurements**: `model.Measurements` is a `ConcurrentDictionary>`. Each `MeasurementResult` has `.Success`, `.Value`, and `.Name`. Always check `.Success` before reading `.Value`. + +### AC Analysis: Voltage Export Types + +In AC analysis, voltages are complex numbers. The export type in `.SAVE` determines what value you get: + +| Directive | Returns | Use for | +|-----------|---------|---------| +| `.SAVE V(out)` | Real part of complex voltage | Almost never what you want | +| `.SAVE VM(out)` | Magnitude (linear) | Frequency response plots | +| `.SAVE VDB(out)` | Magnitude in dB (20×log10) | Gain/attenuation in dB | +| `.SAVE VP(out)` | Phase in degrees | Phase response | + +**Always use `VM()` or `VDB()` for AC magnitude response, never plain `V()`.** + +### Matched-System Insertion Loss Convention + +In a 50-Ohm matched system with a 1V AC source, the maximum voltage at the load is 0.5V due to the source-impedance voltage divider. This 0.5V is the **0 dB insertion loss** reference, not 1V. Insertion loss is calculated as: + +``` +IL_dB = -20 * log10(VM(out) / 0.5) +``` + +A common mistake is comparing `VM(out)` to 1.0 and concluding there is 6 dB of loss when the circuit is actually lossless. + +--- + +## Phase 5b: Tolerance / Monte Carlo Analysis + +After the nominal design passes all tests, verify robustness. + +1. Add additional test methods using `.STEP` or `.MC` netlists: + - Test with ±5% component tolerances (resistors, capacitors) + - Test with ±10% if the design should be production-robust + +2. Assert that specs are still met at worst-case corners. + +3. Example test method name: `RCFilter_MeetsSpecWith10PercentTolerance` + +4. If margins are too tight, go back to Phase 3 and adjust component values for more headroom — do not just widen the tolerance in the assertion. + +--- + +## Phase 6: Circuit Documentation + +After the design passes all tests (nominal and tolerance), generate comprehensive documentation in `circuits//documentation.md`. This document should be understandable by someone who has not been following the design process. + +### Documentation Template + +```markdown +# + +## Summary + +<2-3 sentence plain-language explanation of what this circuit does, who would use it, and what problem it solves. No jargon — a hobbyist or student should understand this paragraph.> + +## How It Works + + + +### Block Diagram + + + + +## Circuit Schematic (Netlist) + + + +## Design Details + +### Topology: + +**Why this topology was chosen:** + +### Component Values and Their Roles + +| Component | Value | Role in Circuit | Governing Equation | +|-----------|-------|-----------------|-------------------| +| R1 | ... | ... | ... | +| C1 | ... | ... | ... | +| ... | ... | ... | ... | + +### Key Design Equations + + + +## Performance + +### Measured Specifications + +| Parameter | Target | Measured | Unit | Status | +|-----------|--------|----------|------|--------| +| ... | ... | ... | ... | PASS/FAIL | + +### Tolerance Analysis + + + +## Design Trade-offs and Limitations + + + +## Potential Modifications + + + +## References + + +``` + +### Documentation Rules + +- **Start simple, add depth progressively.** The Summary and "How It Works" sections should be readable by a beginner. The Design Details section is for engineers who want to reproduce or modify the circuit. +- **Every component value must be justified** — the table should make it clear why each value was chosen, not just what it is. +- **Include actual measured numbers** from the passing test suite, not just theoretical predictions. +- **Keep it honest** — document limitations, trade-offs, and areas where the design is sensitive or approximate. + +--- + +## Phase 7: Human Feedback Loop + +Present results to the user in a structured format. + +### Results Presentation + +1. **Spec vs. Measured table**: + +| Spec | Required | Measured | Tolerance | Status | +|------|----------|----------|-----------|--------| +| ... | ... | ... | ... | PASS/FAIL | + +2. **Tolerance analysis summary**: worst-case margins from Phase 5b. + +3. **Test results**: `dotnet test` output summary — X passed, Y failed. + +### Iteration + +- If specs are not met: propose specific, analytically justified changes. Explain *why* the change will fix the issue. Never blindly tweak. +- Accept user feedback: + - **Approve** → mark design as DONE in backlog + - **Modify specs** → update `requirements.md`, revision history, and re-run from Phase 3 + - **Request changes** → adjust topology or values per feedback, re-run from appropriate phase + - **Documentation feedback** → update `circuits//documentation.md` per user comments +- Update `circuits//results.md` with each iteration. + +--- + +## Phase 8: Backlog Management + +Maintain `backlog.md` as a global tracker across all active circuit designs. + +### Format + +```markdown +# Circuit Design Backlog + +## Active Designs + +### +- [x] Requirements gathered +- [x] Topology selected: +- [ ] Component values calculated +- [ ] Netlist created +- [ ] Tests passing: nominal +- [ ] Tests passing: tolerance +- [ ] Documentation generated +- [ ] Human review approved + +### +- [x] Requirements gathered +- [ ] Topology selected +... + +## Future Tasks +- [ ] () +- [ ] (general) +``` + +- Update at every phase transition +- Multiple designs can be in progress simultaneously at different phases +- Mark completed designs with a `DONE` label and date + +--- + +## Known Limitations & Problematic Areas + +Be honest with the user about what SpiceSharp handles well and what is problematic. + +### What Works Well +- RC/RL/RLC filters (passive and active) +- Amplifier stages: CE, CB, CC, CS, common-gate, differential pairs +- Diode circuits: rectifiers, clippers, clampers, envelope detectors +- Voltage regulators (Zener, series pass) +- AM envelope detection +- Wien bridge and phase-shift oscillators at audio frequencies +- DC power supply circuits +- BJT and MOSFET biasing and small-signal analysis + +### Problematic — Use With Caution + +**FM Demodulation / PLLs** +Phase-locked loops are very difficult in transient SPICE — convergence issues and extremely long simulation times. Prefer simpler discriminator circuits or behavioral-source (B element) approximations for the VCO and phase detector. + +**RF Mixers** +Nonlinear mixing requires very small timesteps in `.TRAN`, making simulations slow. Keep carrier frequencies as low as practical for demonstration purposes. Use behavioral sources (B elements) to model ideal multiplication when full transistor-level mixing is impractical. + +**High-Frequency Effects (>100 MHz)** +SpiceSharp does not model parasitic inductance, skin effect, or transmission line coupling beyond the basic T-line element. Designs above ~100 MHz should be treated as approximate. For RF work, keep frequencies as low as the design allows. + +**Crystal Oscillators** +Crystal equivalent circuits (motional L/C/R + shunt C) work in principle, but startup transients can take millions of cycles to reach steady state — simulation time may be impractical. Consider using a pre-initialized `.IC` condition or shorter simulation with initial perturbation. + +**Op-Amp Macromodels** +SpiceSharp has no built-in op-amp device. You must build from discrete transistors or use behavioral voltage sources (E or B elements). A 5-transistor differential pair model is often sufficient for basic circuits. Document the limitations of your macromodel (bandwidth, slew rate, output swing). + +### Convergence Tips +- Start with `.OPTIONS reltol=1e-3 abstol=1e-12 gmin=1e-12` +- Use `.IC V(node)=` or `.NODESET V(node)=` for known DC operating points +- Add 1GΩ resistors from floating nodes to ground +- For oscillators, apply a small initial perturbation: `.IC V(tank)=0.1` +- Reduce `.TRAN` timestep if waveforms look jagged or simulation diverges +- For stiff circuits, try `.OPTIONS method=gear` + +--- + +## Reference Test Suites + +When writing tests or looking for inspiration on what/how to test, consult the existing test suites in SpiceSharp and SpiceSharpParser. + +### SpiceSharp (`d:\dev\SpiceSharp\SpiceSharpTest\`) + +- `BasicExampleTests.cs` — basic circuit construction and simulation patterns +- `Helper.cs` — tolerance utilities (relative + absolute bounds) +- `Models/Framework.cs` — base test class with `AnalyzeOp`, `AnalyzeDC`, `AnalyzeAC` helpers +- `Models/RLC/` — component-level tests (resistor, capacitor, inductor, mutual inductance) +- `Models/Semiconductors/` — BJT, MOSFET, JFET, diode tests +- `Examples/` — custom component implementations (nonlinear resistor, simple diode) + +### SpiceSharpParser (`d:\dev\SpiceSharpParser\src\SpiceSharpParser.IntegrationTests\`) + +- `BaseTests.cs` — integration test utilities (parsing, simulation running, result extraction) +- `Components/` — netlist-based component tests (resistor, capacitor, BJT, MOSFET, subcircuits, behavioral sources) +- `DotStatements/` — tests for `.DC`, `.AC`, `.TRAN`, `.OP`, `.MEAS`, `.STEP`, `.MC`, `.SAVE`, `.FUNC`, `.PARAM`, etc. +- `Waveforms/` — SIN, PULSE, PWL, AM, SFFM source tests +- `Stochastic/` — Monte Carlo (DEV/LOT) tests +- `AnalogBehavioralModeling/` — POLY, VALUE, TABLE, LAPLACE tests +- `Examples/Circuits/*.cir` — example circuit files (band-pass filters, MOSFET circuits, parameterized subcircuits) + +### Key patterns worth adopting + +- **Tolerance-based assertions** with both relative and absolute bounds (see `Helper.cs` in SpiceSharp) +- **Reference function comparison** — define expected output as a math function and compare against simulation results +- **Measurement validation** via `.MEAS` statements checked programmatically +- **String-array netlist construction** for inline test netlists (SpiceSharpParser pattern) + +--- + +## Debugging Guide + +**Netlist won't parse** +- Verify title line is present (first line, no dot prefix) +- Verify `.END` is present +- Check component syntax: `R1 node1 node2 1k` (name, positive node, negative node, value) +- Ensure no tabs in unexpected places; use spaces +- Check for leading whitespace from C# `@"..."` multiline string indentation — `ParseAndRead` must trim lines + +**Simulation won't converge** +- Check for floating nodes (every node needs a DC path to ground) +- Check for voltage source loops or current source cutsets +- Add `.OPTIONS` with relaxed tolerances +- Try `.NODESET` to provide initial guesses +- Simplify the circuit and add complexity incrementally + +**Simulation returns zero/empty results** +- Verify the 3-step run pattern is used: `Run()` then `InvokeEvents()` then `ToArray()` +- Without `InvokeEvents()`, `EventExportData` handlers never fire and you get 0 data points +- Verify exports are filtered by simulation: `model.Exports.Where(ex => ex.Simulation == simulation)` +- For AC analysis, verify you use `VM()` / `VDB()` / `VP()` instead of plain `V()` in `.SAVE` + +**Results don't match expectations** +- Verify `.MODEL` parameters are realistic for the device +- Check node connectivity — a common error is swapped collector/emitter or drain/source +- Verify units: SpiceSharp uses SI base units (Volts, Amps, Ohms, Farads, Henries) +- For AC analysis: remember gain is magnitude ratio, not dB, unless you convert explicitly +- For transient: ensure simulation runs long enough to reach steady state From 4e80b9dc019e7d3aed08c730054387760bae76af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Wed, 18 Mar 2026 21:21:35 +0100 Subject: [PATCH 2/3] Enhance SKILL.md with additional methods and examples for circuit testing and measurement assertions --- .../skills/spicesharp-circuit-design/SKILL.md | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/.claude/skills/spicesharp-circuit-design/SKILL.md b/.claude/skills/spicesharp-circuit-design/SKILL.md index 0a6aec49..46bfeac5 100644 --- a/.claude/skills/spicesharp-circuit-design/SKILL.md +++ b/.claude/skills/spicesharp-circuit-design/SKILL.md @@ -71,18 +71,20 @@ tests/ ```csharp using System.Text; using SpiceSharp.Simulations; - using SpiceSharpParser; + using SpiceSharpParser; // SpiceSharpReader (simple path) using SpiceSharpParser.Common; - using SpiceSharpParser.ModelReaders.Netlist.Spice; + using SpiceSharpParser.ModelReaders.Netlist.Spice; // SpiceNetlistReader (advanced path) ``` 2. Create `CircuitTestHelper.cs` with shared methods: - `ParseAndRead(string netlist)` → parses, reads, validates, returns `SpiceSharpModel` - `RunOP(string netlist)` → returns `Dictionary` of export values - `RunDC(string netlist)` → returns `Dictionary>` - - `RunAC(string netlist)` → returns `Dictionary>` + - `RunAC(string netlist)` → returns `Dictionary>` (no AC helper in `BaseTests.cs` — implement using `((AC)simulation).Frequency`) - `RunTran(string netlist)` → returns `Dictionary>` - `GetMeasurements(string netlist)` → returns `Dictionary` of successful .MEAS results + - `AssertMeasurement(SpiceSharpModel model, string name, double expectedValue)` → checks measurement exists, succeeded, and value is within tolerance (RelTol=1e-3, AbsTol=1e-12). Mirrors `BaseTests.AssertMeasurement` from `src/SpiceSharpParser.IntegrationTests/BaseTests.cs`. + - `AssertMeasurementSuccess(SpiceSharpModel model, string name)` → checks measurement exists and succeeded (for measurements where you only care it found a value) - Each method handles: parse → read → validate → attach exports → run with InvokeEvents → collect results 3. Verify the toolchain by writing and running a trivial RC filter test with assertions. @@ -250,14 +252,26 @@ Title Line (required, first line) **Devices**: R, C, L, K (mutual inductance), D (diode), Q (BJT), M (MOSFET), J (JFET), V (voltage source), I (current source), E (VCVS), F (CCCS), G (VCCS), H (CCVS), B (behavioral), S (voltage switch), W (current switch), T (transmission line) +**Behavioral Sources (B element)** +B elements define voltage or current as arbitrary expressions of other circuit variables: +```spice +B1 out 0 V={V(in1)*V(in2)} * Voltage = product of two node voltages (ideal multiplier) +B2 out 0 I={V(ctrl)/1k} * Current = voltage-controlled (ideal VCCS) +B3 out 0 V={IF(V(in)>0.5, 5, 0)} * Conditional logic (ideal comparator) +B4 out 0 V={V(in)*sin(6.28*1e6*TIME)} * Time-dependent (ideal mixer/modulator) +``` +B elements are the recommended approach for modeling ideal functional blocks (op-amps, multipliers, comparators, VCOs) when full transistor-level simulation is unnecessary or impractical. See `src/SpiceSharpParser.IntegrationTests/AnalogBehavioralModeling/` tests for more examples. + **Value suffixes**: `T`=1e12, `G`=1e9, `MEG`=1e6, `k`=1e3, `m`=1e-3, `u`=1e-6, `n`=1e-9, `p`=1e-12, `f`=1e-15 -**Analyses**: `.OP`, `.DC`, `.AC DEC|OCT|LIN `, `.TRAN [tstart] [UIC]`, `.NOISE` +**Analyses**: `.OP`, `.DC`, `.AC DEC|OCT|LIN `, `.TRAN [tstart] [UIC]`, `.NOISE V() ` (limited — no RunNoise helper; only verified to not throw exceptions) **Output**: `.SAVE V() I()`, `.MEAS `, `.PRINT` **Controls**: `.PARAM`, `.FUNC`, `.SUBCKT`/`.ENDS`, `.INCLUDE`, `.LIB`, `.STEP`, `.MC`, `.MODEL`, `.IC`, `.NODESET`, `.OPTIONS` +**Expressions**: Use `{expression}` syntax in component values when `.PARAM` defines the variables (e.g., `R1 in out {Rval*2}` with `.PARAM Rval=1k`). Expressions support arithmetic, built-in math functions, and references to other parameters. + **Waveforms**: `DC `, `AC [phase]`, `SIN(offset amp freq [delay damping phase])`, `PULSE(v1 v2 td tr tf pw period)`, `PWL(t1 v1 t2 v2 ...)`, `AM(amp freq fc [delay phase])`, `SFFM(offset amp fc mod_index fsig)` ### Netlist Rules @@ -461,15 +475,33 @@ public void Hypothesis_R1_Controls_CutoffFrequency() netlist.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0)); ``` + **Alternative: String-array construction** (avoids trimming entirely): + ```csharp + public static SpiceSharpModel ParseAndRead(params string[] lines) + { + var text = string.Join(Environment.NewLine, lines); + // ... parse and read as below ... + } + ``` + This is the pattern used by `BaseTests.GetSpiceSharpModel()` — each line is a standalone string, so no trimming is needed. + 2. **Parse** with required settings (without these, parsing fails or produces wrong results): ```csharp var parser = new SpiceNetlistParser(); parser.Settings.Lexing.HasTitle = true; - parser.Settings.Parsing.IsEndRequired = true; + parser.Settings.Parsing.IsEndRequired = true; // Without this, parser silently accepts incomplete netlists missing .END var parseResult = parser.ParseNetlist(trimmed); ``` -3. **Read** using `SpiceNetlistReader` (not `SpiceSharpReader`) with required settings: +3. **Read** — two options: + + **Simple path (recommended)** — `SpiceSharpReader` with sensible defaults: + ```csharp + var reader = new SpiceSharpReader(); + var model = reader.Read(parseResult.FinalModel); + ``` + + **Advanced path** — `SpiceNetlistReader` when you need custom settings (working directory, encoding, random seed): ```csharp var readerSettings = new SpiceNetlistReaderSettings( new SpiceNetlistCaseSensitivitySettings(), @@ -503,6 +535,19 @@ public void Hypothesis_R1_Controls_CutoffFrequency() codes.ToArray(); // forces enumeration ``` + **Multi-analysis netlists:** If a netlist contains multiple analysis types (e.g., `.OP` and `.AC`), iterate all simulations: + ```csharp + foreach (var simulation in model.Simulations) + { + var exports = model.Exports.Where(ex => ex.Simulation == simulation).ToList(); + // ... attach EventExportData handlers for these exports ... + var codes = simulation.Run(model.Circuit, -1); + codes = simulation.InvokeEvents(codes); + codes.ToArray(); + } + ``` + The `RunOP`/`RunDC`/`RunAC`/`RunTran` helpers assume a single simulation of the expected type. + 7. **Extract sweep/time/frequency values** per simulation type: - DC sweep value: `((DC)simulation).GetCurrentSweepValue().Last()` - AC frequency: `((AC)simulation).Frequency` (cast to `AC`, not `FrequencySimulation`) @@ -510,6 +555,20 @@ public void Hypothesis_R1_Controls_CutoffFrequency() 8. **Measurements**: `model.Measurements` is a `ConcurrentDictionary>`. Each `MeasurementResult` has `.Success`, `.Value`, and `.Name`. Always check `.Success` before reading `.Value`. +### Tolerance Comparison Patterns + +**For spec verification** (values from requirements.md): +Use `Assert.InRange(actual, min, max)` with explicit bounds from the spec table. + +**For reference function comparison** (comparing simulation output against an analytical formula): +Use the relative+absolute tolerance pattern from `BaseTests.cs`: +```csharp +double tol = Math.Max(Math.Abs(actual), Math.Abs(expected)) * 1e-3 + 1e-12; +Assert.True(Math.Abs(expected - actual) < tol, + $"Expected {expected}, got {actual}, tolerance {tol}"); +``` +This handles both large and small values correctly — the relative term (1e-3) dominates for large values, while the absolute term (1e-12) prevents false failures near zero. + ### AC Analysis: Voltage Export Types In AC analysis, voltages are complex numbers. The export type in `.SAVE` determines what value you get: @@ -727,6 +786,8 @@ SpiceSharp has no built-in op-amp device. You must build from discrete transisto ### Convergence Tips - Start with `.OPTIONS reltol=1e-3 abstol=1e-12 gmin=1e-12` +- For circuits that fail to find a DC operating point, increase iteration limits: `.OPTIONS itl1=200` (default ~100) +- For transient convergence issues, increase per-timepoint iterations: `.OPTIONS itl4=50` (default ~10) - Use `.IC V(node)=` or `.NODESET V(node)=` for known DC operating points - Add 1GΩ resistors from floating nodes to ground - For oscillators, apply a small initial perturbation: `.IC V(tank)=0.1` From b16978fce63aa945d78d487fbbc0e0b0e829c70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Thu, 19 Mar 2026 01:08:20 +0100 Subject: [PATCH 3/3] Add discoveries and enhance SKILL.md; implement voltage export tests and update export logic --- .../skills/spicesharp-circuit-design/SKILL.md | 5 +- .../spicesharp-circuit-design/discoveries.md | 34 +++ .../VoltageExportTests.cs | 217 ++++++++++++++++++ .../VoltageExports/VoltageDecibelExport.cs | 2 +- .../VoltageExports/VoltagePhaseExport.cs | 2 +- .../VoltageExports/VoltageRealExport.cs | 12 +- src/SpiceSharpParser/SpiceSharpParser.csproj | 2 +- 7 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 .claude/skills/spicesharp-circuit-design/discoveries.md create mode 100644 src/SpiceSharpParser.IntegrationTests/VoltageExportTests.cs diff --git a/.claude/skills/spicesharp-circuit-design/SKILL.md b/.claude/skills/spicesharp-circuit-design/SKILL.md index 46bfeac5..795fc3f5 100644 --- a/.claude/skills/spicesharp-circuit-design/SKILL.md +++ b/.claude/skills/spicesharp-circuit-design/SKILL.md @@ -30,6 +30,7 @@ The workbench supports multiple concurrent circuit designs. Maintain this struct ``` backlog.md — global task tracker across all active designs +discoveries.md — key lessons learned across designs (read before starting work) circuits/ / requirements.md — quantitative specs, constraints, acceptance criteria @@ -45,7 +46,7 @@ tests/ Tests.cs — xUnit test class per circuit design ``` -**On every invocation**, read `backlog.md` first to understand what work is in progress across all active designs. Each backlog task references which circuit it belongs to. +**On every invocation**, read `backlog.md` and `discoveries.md` first — the backlog shows what work is in progress, and discoveries contain hard-won lessons that prevent repeating past mistakes. --- @@ -135,7 +136,7 @@ Before designing anything, build a complete specification. - [ ] .TRAN — ## Acceptance Criteria - + ## Constraints diff --git a/.claude/skills/spicesharp-circuit-design/discoveries.md b/.claude/skills/spicesharp-circuit-design/discoveries.md new file mode 100644 index 00000000..8a78f02a --- /dev/null +++ b/.claude/skills/spicesharp-circuit-design/discoveries.md @@ -0,0 +1,34 @@ +# Key Discoveries + +Lessons learned during circuit design that apply across projects. Read this before starting any new design. + +--- + +## 1. `.TRAN` tmax is required for large-cap diode circuits + +**Problem:** Simulation produces wildly wrong output voltages (e.g., 0.8V instead of 22V) and completes suspiciously fast. + +**Root Cause:** SpiceSharp's adaptive timestep takes ~18ms steps with large capacitors (millifarad range), completely skipping over the sub-millisecond diode conduction windows. The diodes never conduct, so the capacitors never charge. + +**Solution:** Always specify the `tmax` parameter (4th argument) in `.TRAN`: +```spice +.TRAN 100u 2 0 100u UIC +* ^step ^stop ^start ^tmax +``` +Rule of thumb: tmax ≤ 1/10 of the AC period (for 50Hz: tmax ≤ 2ms; 100µs works well). + +--- + +## 2. Prefer `.MEAS` over `.SAVE` for automated spec verification + +**Problem:** Using `.SAVE` to collect raw waveform data requires complex post-processing in test code to extract specs (e.g., finding threshold crossings, computing averages over windows). + +**Root Cause:** `.SAVE` gives raw data points; all analysis logic must be written in C#. This is error-prone and verbose. + +**Solution:** Use `.MEAS` directives in the netlist to extract specs directly: +```spice +.MEAS TRAN v_out_avg AVG V(out) FROM=1.5 TO=2.0 +.MEAS TRAN ripple_pp PARAM='v_out_max - v_out_min' +.MEAS TRAN settle_95 WHEN V(out) = 21.47 RISE=1 +``` +Then in tests: `var meas = CircuitTestHelper.GetMeasurements(netlist);` and assert on named values. diff --git a/src/SpiceSharpParser.IntegrationTests/VoltageExportTests.cs b/src/SpiceSharpParser.IntegrationTests/VoltageExportTests.cs new file mode 100644 index 00000000..f440e8e3 --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/VoltageExportTests.cs @@ -0,0 +1,217 @@ +using System; +using System.Linq; +using Xunit; + +namespace SpiceSharpParser.IntegrationTests +{ + public class VoltageExportTests : BaseTests + { + /// + /// RC low-pass filter: R=1k, C=159nF => fc ≈ 1 kHz. + /// At DC (1 Hz), gain ≈ 1 so VDB ≈ 0 dB. + /// At high frequency (1 MHz), gain << 1 so VDB << 0 dB. + /// Verifies VDB uses 20*log10 (not bare log10). + /// + [Fact] + public void VDB_ReturnsCorrect20Log10_ForRCFilter() + { + var model = GetSpiceSharpModel( + "VDB test - RC low-pass", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vdb_at_dc FIND VDB(OUT) AT=1", + ".MEAS AC vdb_at_fc FIND VDB(OUT) AT=1e3", + ".END"); + + RunSimulations(model); + + // At 1 Hz (essentially DC), magnitude ≈ 1, so VDB ≈ 0 dB + AssertMeasurementSuccess(model, "vdb_at_dc"); + double vdbDc = model.Measurements["vdb_at_dc"][0].Value; + Assert.True(Math.Abs(vdbDc) < 0.1, $"VDB at DC should be ~0 dB, got {vdbDc}"); + + // At cutoff (1 kHz), magnitude ≈ 1/sqrt(2), so VDB ≈ -3.01 dB + AssertMeasurementSuccess(model, "vdb_at_fc"); + double vdbFc = model.Measurements["vdb_at_fc"][0].Value; + Assert.True(Math.Abs(vdbFc - (-3.01)) < 0.5, + $"VDB at cutoff should be ~-3 dB, got {vdbFc}"); + } + + /// + /// Without the 20x multiplier, log10(1) = 0 would still pass, + /// but log10(0.707) = -0.15 which is NOT -3 dB. + /// This test catches the missing multiplier by checking magnitude well below unity. + /// + [Fact] + public void VDB_HighFrequency_NotBareLog10() + { + var model = GetSpiceSharpModel( + "VDB multiplier test", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vdb_high FIND VDB(OUT) AT=100e3", + ".END"); + + RunSimulations(model); + + AssertMeasurementSuccess(model, "vdb_high"); + double vdbHigh = model.Measurements["vdb_high"][0].Value; + + // At 100 kHz (100x fc), gain ≈ 1/100, VDB ≈ -40 dB + // Without 20x multiplier, bare log10(0.01) = -2, which is > -10 + Assert.True(vdbHigh < -30, + $"VDB at 100kHz should be << -30 dB (around -40), got {vdbHigh}. " + + "If ~-2, the 20*log10 multiplier is missing."); + } + + /// + /// At DC, phase should be ~0. + /// At frequencies well above cutoff, phase should approach -pi/2 (-90°). + /// Verifies VP returns phase (radians), not magnitude. + /// + [Fact] + public void VP_ReturnsPhase_NotMagnitude() + { + var model = GetSpiceSharpModel( + "VP test - RC low-pass", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vp_at_dc FIND VP(OUT) AT=1", + ".MEAS AC vp_at_fc FIND VP(OUT) AT=1e3", + ".MEAS AC vp_at_high FIND VP(OUT) AT=100e3", + ".END"); + + RunSimulations(model); + + // At DC: phase ≈ 0 + AssertMeasurementSuccess(model, "vp_at_dc"); + double vpDc = model.Measurements["vp_at_dc"][0].Value; + Assert.True(Math.Abs(vpDc) < 0.01, + $"VP at DC should be ~0 radians, got {vpDc}"); + + // At cutoff: phase ≈ -pi/4 (-0.785 rad) + AssertMeasurementSuccess(model, "vp_at_fc"); + double vpFc = model.Measurements["vp_at_fc"][0].Value; + Assert.True(Math.Abs(vpFc - (-Math.PI / 4)) < 0.1, + $"VP at cutoff should be ~-0.785 rad, got {vpFc}"); + + // At high freq: phase ≈ -pi/2 (-1.571 rad) + // If VP returned magnitude instead, it would be a small positive number (~0.01) + AssertMeasurementSuccess(model, "vp_at_high"); + double vpHigh = model.Measurements["vp_at_high"][0].Value; + Assert.True(vpHigh < -1.0, + $"VP at high freq should be near -pi/2 (~-1.57), got {vpHigh}. " + + "If positive, VP is returning magnitude instead of phase."); + } + + /// + /// VR should return the real part of complex voltage. + /// At DC, VR ≈ 1 (full voltage, no imaginary component). + /// At cutoff, VR ≈ 0.5 (real part of 1/(1+j) = 0.5 - 0.5j). + /// + [Fact] + public void VR_ReturnsRealPart_NotMagnitude() + { + var model = GetSpiceSharpModel( + "VR test - RC low-pass", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vr_at_dc FIND VR(OUT) AT=1", + ".MEAS AC vr_at_fc FIND VR(OUT) AT=1e3", + ".MEAS AC vm_at_fc FIND VM(OUT) AT=1e3", + ".END"); + + RunSimulations(model); + + // At DC: VR ≈ 1 + AssertMeasurementSuccess(model, "vr_at_dc"); + double vrDc = model.Measurements["vr_at_dc"][0].Value; + Assert.True(Math.Abs(vrDc - 1.0) < 0.01, + $"VR at DC should be ~1.0, got {vrDc}"); + + // At cutoff: VR ≈ 0.5, VM ≈ 0.707 + // VR != VM proves we're getting the real part, not magnitude + AssertMeasurementSuccess(model, "vr_at_fc"); + AssertMeasurementSuccess(model, "vm_at_fc"); + double vrFc = model.Measurements["vr_at_fc"][0].Value; + double vmFc = model.Measurements["vm_at_fc"][0].Value; + + Assert.True(Math.Abs(vrFc - 0.5) < 0.05, + $"VR at cutoff should be ~0.5, got {vrFc}"); + Assert.True(Math.Abs(vmFc - 0.707) < 0.05, + $"VM at cutoff should be ~0.707, got {vmFc}"); + Assert.True(Math.Abs(vrFc - vmFc) > 0.1, + $"VR ({vrFc}) should differ from VM ({vmFc}) at cutoff"); + } + + /// + /// Cross-check: VR² + VI² should equal VM² (Pythagorean identity). + /// This validates that VR and VI are the true real/imaginary components. + /// + [Fact] + public void VR_And_VI_Satisfy_PythagoreanIdentity() + { + var model = GetSpiceSharpModel( + "VR/VI/VM identity test", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vr_fc FIND VR(OUT) AT=1e3", + ".MEAS AC vi_fc FIND VI(OUT) AT=1e3", + ".MEAS AC vm_fc FIND VM(OUT) AT=1e3", + ".END"); + + RunSimulations(model); + + AssertMeasurementSuccess(model, "vr_fc"); + AssertMeasurementSuccess(model, "vi_fc"); + AssertMeasurementSuccess(model, "vm_fc"); + + double vr = model.Measurements["vr_fc"][0].Value; + double vi = model.Measurements["vi_fc"][0].Value; + double vm = model.Measurements["vm_fc"][0].Value; + + double computedMag = Math.Sqrt(vr * vr + vi * vi); + Assert.True(Math.Abs(computedMag - vm) < 1e-6, + $"sqrt(VR²+VI²) = {computedMag} should equal VM = {vm}"); + } + + /// + /// VDB should equal 20*log10(VM) — cross-check between two export types. + /// + [Fact] + public void VDB_Equals_20Log10_VM() + { + var model = GetSpiceSharpModel( + "VDB vs VM cross-check", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC vdb_val FIND VDB(OUT) AT=10e3", + ".MEAS AC vm_val FIND VM(OUT) AT=10e3", + ".END"); + + RunSimulations(model); + + AssertMeasurementSuccess(model, "vdb_val"); + AssertMeasurementSuccess(model, "vm_val"); + + double vdb = model.Measurements["vdb_val"][0].Value; + double vm = model.Measurements["vm_val"][0].Value; + double expected = 20.0 * Math.Log10(vm); + + Assert.True(Math.Abs(vdb - expected) < 1e-6, + $"VDB ({vdb}) should equal 20*log10(VM) ({expected})"); + } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageDecibelExport.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageDecibelExport.cs index 88f29355..d38027ec 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageDecibelExport.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageDecibelExport.cs @@ -63,7 +63,7 @@ public override double Extract() return double.NaN; } - return Math.Log10(ExportImpl.Value.Magnitude); + return 20.0 * Math.Log10(ExportImpl.Value.Magnitude); } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltagePhaseExport.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltagePhaseExport.cs index f98c781e..7006de2a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltagePhaseExport.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltagePhaseExport.cs @@ -61,7 +61,7 @@ public override double Extract() return double.NaN; } - return ExportImpl.Value.Magnitude; + return ExportImpl.Value.Phase; } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageRealExport.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageRealExport.cs index aef7475f..970c2730 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageRealExport.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageRealExport.cs @@ -21,7 +21,7 @@ public VoltageRealExport(string name, ISimulationWithEvents simulation, string n Name = name ?? throw new System.ArgumentNullException(nameof(name)); Node = node ?? throw new System.ArgumentNullException(nameof(node)); Reference = reference; - ExportImpl = new RealVoltageExport((IBiasingSimulation)simulation, node, reference); + ExportImpl = new ComplexVoltageExport((FrequencySimulation)simulation, node, reference); } /// @@ -40,15 +40,15 @@ public VoltageRealExport(string name, ISimulationWithEvents simulation, string n public override string QuantityUnit => "Voltage (V)"; /// - /// Gets the real voltage export that provides voltage + /// Gets the complex voltage export that provides voltage real part /// - protected RealVoltageExport ExportImpl { get; } + protected ComplexVoltageExport ExportImpl { get; } /// - /// Extracts the voltage at the main node + /// Extracts the real part of the voltage at the main node /// /// - /// A voltage (real) at the main node + /// The real part of the complex voltage at the main node /// public override double Extract() { @@ -62,7 +62,7 @@ public override double Extract() return double.NaN; } - return ExportImpl.Value; + return ExportImpl.Value.Real; } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/SpiceSharpParser.csproj b/src/SpiceSharpParser/SpiceSharpParser.csproj index 4da60a38..749b53af 100644 --- a/src/SpiceSharpParser/SpiceSharpParser.csproj +++ b/src/SpiceSharpParser/SpiceSharpParser.csproj @@ -22,7 +22,7 @@ MIT latest - 3.2.12 + 3.3.0