Developer Guide¶
Welcome to the GeckoCIRCUITS developer guide! This document explains how to extend the simulator with new circuit components, control blocks, and features.
Overview¶
GeckoCIRCUITS is a Java 21 power electronics circuit simulator built on Modified Nodal Analysis (MNA) for electrical simulation. The architecture is modular and designed for extension. Before diving into specific extensions, read /CLAUDE.md in the repository root for architecture overview and build commands.
Setup: Clone the repository, run mvn clean install, then open the project in your IDE (NetBeans recommended for GUI development).
Architecture Overview¶
GeckoCIRCUITS
├── Entry Point: GeckoSim (gecko.GeckoSim)
│ └── Operating Modes: STANDALONE (GUI), REMOTE (RMI), MMF, SIMULINK
│
├── Circuit Simulation Engine
│ ├── circuit/matrix/ → MNA matrix stamping (IMatrixStamper, StamperRegistry)
│ ├── circuit/netlist/ → Netlist building & component definitions
│ ├── circuit/simulation/ → Solver & integration (Backward Euler, Trapezoidal, Gear-Shichman)
│ └── circuit/components/ → 50+ circuit component implementations
│
├── Control Blocks
│ ├── control/calculators/ → 80+ control block classes (PI, PID, Gain, etc.)
│ └── calculators/ → Signal processors & transformations
│
├── Supporting Modules
│ ├── datacontainer/ → Signal storage with caching optimization
│ ├── math/ → Matrix operations, LU decomposition, FFT
│ ├── thermal/ → Thermal network simulation (MNA-based)
│ ├── scope/ → Oscilloscope visualization
│ └── i18n/ → Internationalization (968+ translation keys)
│
└── External Integration
├── GeckoRemoteInterface (RMI for MATLAB/Octave)
├── GeckoCustomMMF (Memory-mapped files for high-speed data)
└── GeckoRemoteRegistry (RMI registry management)
Key Domains¶
| Domain | Purpose | Extension Point | Difficulty |
|---|---|---|---|
| Control Blocks | Signal processing, PID, gains, logic | Extend AbstractControlCalculatable | Easy |
| Circuit Components | Resistors, capacitors, switches, motors | Implement IMatrixStamper/IStatefulStamper | Medium |
| Matrix Operations | MNA equation solving | Modify LKMatrices, solver implementations | Hard |
| Thermal Network | Heat dissipation simulation | Similar to circuit domain (MNA-based) | Medium |
Adding a Control Block Calculator¶
Control blocks are the easiest extension point. They process input signals and produce output signals based on mathematical operations (gains, integrations, PID control, etc.).
How the Control Block System Works¶
- Base Class:
AbstractControlCalculatable(1 input, 1 output) or multi-input variants - Calculation: Called each time step by
berechneYOUT(deltaT)method - Inputs/Outputs: Signal arrays
_inputSignaland_outputSignal(2D arrays for multi-port) - State: Optional; managed by subclasses for integrators, delays, etc.
Example: Simple Gain Amplifier¶
Here's how GainCalculator (the simplest calculator) works:
// File: control/calculators/GainCalculator.java
public final class GainCalculator extends AbstractSingleInputSingleOutputCalculator {
private double _gain;
public GainCalculator(final double gain) {
super(); // Initializes 1 input, 1 output signal array
setGain(gain);
}
@Override
public void berechneYOUT(final double deltaT) {
// Called every simulation time step
_outputSignal[0][0] = _gain * _inputSignal[0][0];
}
public void setGain(final double gain) {
_gain = gain;
}
}
Key Points: - _inputSignal[portIndex][0] = input value - _outputSignal[portIndex][0] = computed output value - berechneYOUT() is called every Δt seconds - Use AbstractSingleInputSingleOutputCalculator base class for 1-in/1-out blocks
Step-by-Step: Add a Custom Saturation Block¶
Let's add a saturation limiter block (useful for control feedback).
Step 1: Create the calculator class
// File: control/calculators/SaturationCalculator.java
package gecko.geckocircuits.control.calculators;
/**
* Saturation limiter: output = clamp(input, min, max)
*/
public final class SaturationCalculator extends AbstractSingleInputSingleOutputCalculator {
private double _minValue = -1.0;
private double _maxValue = 1.0;
public SaturationCalculator(final double min, final double max) {
super();
setLimits(min, max);
}
@Override
public void berechneYOUT(final double deltaT) {
double input = _inputSignal[0][0];
_outputSignal[0][0] = Math.max(_minValue, Math.min(_maxValue, input));
}
public void setLimits(final double min, final double max) {
if (min > max) {
throw new IllegalArgumentException("Min must be <= Max");
}
_minValue = min;
_maxValue = max;
}
}
Step 2: Register in control block factory (if using GUI)
If you're adding this to the GUI, register it in the control block dialog/factory classes (typically in ReglerGainDialog or similar). Otherwise, instantiate directly in your simulation code.
Step 3: Test the calculator
// File: src/test/java/.../SaturationCalculatorTest.java
public class SaturationCalculatorTest {
@Test
public void testSaturation() {
SaturationCalculator calc = new SaturationCalculator(-10, 10);
// Connect mock input
calc._inputSignal[0] = new double[]{0};
calc.checkInputWithoutConnectionAndFill(0);
// Test clamping
calc._inputSignal[0][0] = 5.0;
calc.berechneYOUT(0.001);
assertEquals(5.0, calc._outputSignal[0][0], 1e-6);
calc._inputSignal[0][0] = 15.0;
calc.berechneYOUT(0.001);
assertEquals(10.0, calc._outputSignal[0][0], 1e-6);
}
}
Multi-Input/Multi-Output Calculator¶
For blocks with multiple inputs or outputs, extend the appropriate base class:
// File: control/calculators/MyMultiInputCalculator.java
public final class MyMultiInputCalculator extends AbstractTwoInputsOneOutputCalculator {
@Override
public void berechneYOUT(final double deltaT) {
double input1 = _inputSignal[0][0];
double input2 = _inputSignal[1][0];
_outputSignal[0][0] = input1 + input2; // Simple adder
}
}
Available Base Classes: - AbstractSingleInputSingleOutputCalculator - 1 in, 1 out (most common) - AbstractTwoInputsOneOutputCalculator - 2 in, 1 out (adders, multipliers) - Multi-input variants in calculators/ package
Stateful Calculators (Integrators, Delays)¶
For blocks that maintain state across time steps:
public final class IntegratorCalculator extends AbstractSingleInputSingleOutputCalculator {
private double _integralSum = 0.0;
@Override
public void berechneYOUT(final double deltaT) {
// Trapezoidal integration rule
double input = _inputSignal[0][0];
_integralSum += input * deltaT;
_outputSignal[0][0] = _integralSum;
}
@Override
public void tearDownOnPause() {
// Called when simulation pauses; reset state if needed
_integralSum = 0.0;
}
public void setInitialValue(final double value) {
_integralSum = value;
}
}
Key Methods: - berechneYOUT(deltaT) - calculation (called every step) - tearDownOnPause() - cleanup when simulation pauses (override if needed) - Private fields store state between calls
Adding a Circuit Component¶
Circuit components use the Modified Nodal Analysis (MNA) technique. This is more complex than control blocks but the stamping pattern is reusable.
How MNA Works¶
GeckoCIRCUITS solves the equation: A · x = b
Where: - A = conductance/admittance matrix (stamped by each component) - x = vector of unknowns (node voltages, branch currents) - b = source/excitation vector (stamped by current/voltage sources)
Each component "stamps" its contribution into these matrices based on its model.
The IMatrixStamper Interface¶
Every circuit component implements this interface:
public interface IMatrixStamper {
/**
* Stamp component's effect into A matrix (conductance)
* @param a A matrix [nodes × nodes]
* @param nodeX, nodeY component nodes
* @param nodeZ auxiliary node (for inductors, voltage sources)
* @param parameter component parameters array
* @param dt time step size (for implicit integration)
*/
void stampMatrixA(double[][] a, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt);
/**
* Stamp component's effect into b vector (sources)
*/
void stampVectorB(double[] b, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt, double time,
double[] previousValues);
/**
* Calculate component current after matrix solution
*/
double calculateCurrent(double nodeVoltageX, double nodeVoltageY,
double[] parameter, double dt, double previousCurrent);
/**
* Weight factor for A-matrix: e.g., 1/R for resistor, C/dt for capacitor
*/
double getAdmittanceWeight(double parameterValue, double dt);
}
Example: Resistor Stamper¶
// Simplified resistor implementation
public class ResistorStamper implements IMatrixStamper {
@Override
public void stampMatrixA(double[][] a, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt) {
double resistance = parameter[0];
double conductance = 1.0 / resistance;
// Stamp conductance at diagonal entries
a[nodeX][nodeX] += conductance;
a[nodeY][nodeY] += conductance;
// Stamp negative conductance at off-diagonal entries
a[nodeX][nodeY] -= conductance;
a[nodeY][nodeX] -= conductance;
}
@Override
public void stampVectorB(double[] b, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt, double time,
double[] previousValues) {
// Resistor is purely passive; no source contribution
}
@Override
public double calculateCurrent(double vx, double vy, double[] parameter,
double dt, double previousCurrent) {
double resistance = parameter[0];
return (vx - vy) / resistance; // Ohm's law
}
@Override
public double getAdmittanceWeight(double parameterValue, double dt) {
return 1.0 / parameterValue; // 1/R
}
}
Step-by-Step: Add a Custom Nonlinear Resistor¶
Let's implement a temperature-dependent resistor: R(T) = R0 * (1 + α * ΔT)
Step 1: Create the stamper
// File: circuit/matrix/TemperatureResistorStamper.java
package gecko.geckocircuits.circuit.matrix;
public class TemperatureResistorStamper implements IMatrixStamper {
@Override
public void stampMatrixA(double[][] a, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt) {
// parameter[0] = base resistance R0
// parameter[1] = temperature coefficient α
// parameter[2] = current temperature T
// parameter[3] = reference temperature T0
double r0 = parameter[0];
double alpha = parameter[1];
double temp = parameter[2];
double tempRef = parameter[3];
double resistance = r0 * (1.0 + alpha * (temp - tempRef));
double conductance = 1.0 / resistance;
a[nodeX][nodeX] += conductance;
a[nodeY][nodeY] += conductance;
a[nodeX][nodeY] -= conductance;
a[nodeY][nodeX] -= conductance;
}
@Override
public void stampVectorB(double[] b, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt, double time,
double[] previousValues) {
// No source term
}
@Override
public double calculateCurrent(double vx, double vy, double[] parameter,
double dt, double previousCurrent) {
double r0 = parameter[0];
double alpha = parameter[1];
double temp = parameter[2];
double tempRef = parameter[3];
double resistance = r0 * (1.0 + alpha * (temp - tempRef));
return (vx - vy) / resistance;
}
@Override
public double getAdmittanceWeight(double parameterValue, double dt) {
// parameterValue is the effective resistance
return 1.0 / parameterValue;
}
}
Step 2: Register in StamperRegistry
// In your simulation initialization
StamperRegistry registry = StamperRegistry.createDefault();
registry.register(CircuitTyp.LK_R, new TemperatureResistorStamper());
Stateful Components: IStatefulStamper¶
Components that can change state (diodes, switches, thyristors) use IStatefulStamper:
public interface IStatefulStamper extends IMatrixStamper {
/** Check if component should turn on/off based on voltages/currents */
void updateState(double vx, double vy, double current, double time);
/** Returns true if state changed (matrix needs re-factorization) */
boolean isStateChanged();
/** Reset state-changed flag after solver convergence */
void resetStateChange();
/** Get current ON/OFF state */
boolean isOn();
/** Force a specific state */
void setState(boolean on);
/** Effective resistance (depends on state) */
double getCurrentResistance();
}
Example: Ideal Switch (On/Off)
public class IdealSwitchStamper implements IStatefulStamper {
private boolean _isOn = false;
private boolean _stateChanged = false;
private static final double ON_RESISTANCE = 0.001; // 1 mΩ
private static final double OFF_RESISTANCE = 1e9; // 1 GΩ
@Override
public void updateState(double vx, double vy, double current, double time) {
// Check if switch should toggle based on external signal
// (would be connected to a control input in practice)
}
@Override
public void stampMatrixA(double[][] a, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt) {
double resistance = _isOn ? ON_RESISTANCE : OFF_RESISTANCE;
double conductance = 1.0 / resistance;
a[nodeX][nodeX] += conductance;
a[nodeY][nodeY] += conductance;
a[nodeX][nodeY] -= conductance;
a[nodeY][nodeX] -= conductance;
}
@Override
public void stampVectorB(double[] b, int nodeX, int nodeY, int nodeZ,
double[] parameter, double dt, double time,
double[] previousValues) {
// No source
}
@Override
public double calculateCurrent(double vx, double vy, double[] parameter,
double dt, double previousCurrent) {
double resistance = _isOn ? ON_RESISTANCE : OFF_RESISTANCE;
return (vx - vy) / resistance;
}
@Override
public double getAdmittanceWeight(double parameterValue, double dt) {
double resistance = _isOn ? ON_RESISTANCE : OFF_RESISTANCE;
return 1.0 / resistance;
}
// IStatefulStamper methods
@Override
public boolean isStateChanged() {
return _stateChanged;
}
@Override
public void resetStateChange() {
_stateChanged = false;
}
@Override
public boolean isOn() {
return _isOn;
}
@Override
public void setState(boolean on) {
boolean old = _isOn;
_isOn = on;
_stateChanged = (old != on);
}
@Override
public double getCurrentResistance() {
return _isOn ? ON_RESISTANCE : OFF_RESISTANCE;
}
}
Component Registration: CircuitTyp Enum¶
New circuit components must be registered in CircuitTyp enum:
// File: circuit/circuitcomponents/CircuitTyp.java
public enum CircuitTyp implements AbstractComponentTyp {
// ... existing types ...
LK_R(1, ResistorCircuit.TYPE_INFO), // Resistor
LK_CUSTOM_NLRES(99, MyCustomNonlinearResistor.TYPE_INFO), // Your component
}
Testing Your Changes¶
GeckoCIRCUITS enforces test coverage and code quality standards.
Running Tests¶
# Run all tests
mvn test
# Run single test class
mvn test -Dtest=GainCalculatorTest
# Generate JaCoCo coverage report (output: target/site/jacoco/index.html)
mvn clean test jacoco:report
# Check coverage meets 60% minimum for core packages
mvn verify
Writing Calculator Tests¶
// File: src/test/java/.../GainCalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class GainCalculatorTest {
@Test
public void testBasicGain() {
GainCalculator calc = new GainCalculator(2.5);
// Initialize input signal array
calc._inputSignal[0] = new double[1];
calc._inputSignal[0][0] = 4.0;
calc.berechneYOUT(0.001); // deltaT doesn't matter for gain
assertEquals(10.0, calc._outputSignal[0][0], 1e-9);
}
@Test
public void testZeroGain() {
GainCalculator calc = new GainCalculator(0.0);
calc._inputSignal[0] = new double[1];
calc._inputSignal[0][0] = 100.0;
calc.berechneYOUT(0.001);
assertEquals(0.0, calc._outputSignal[0][0], 1e-9);
}
@Test
public void testNegativeGain() {
GainCalculator calc = new GainCalculator(-2.0);
calc._inputSignal[0] = new double[1];
calc._inputSignal[0][0] = 3.0;
calc.berechneYOUT(0.001);
assertEquals(-6.0, calc._outputSignal[0][0], 1e-9);
}
}
Writing Circuit Component Tests¶
// File: src/test/java/.../ResistorStamperTest.java
public class ResistorStamperTest {
@Test
public void testStamping() {
ResistorStamper stamper = new ResistorStamper();
double[][] a = new double[3][3];
double[] parameter = {100.0}; // 100 Ω
stamper.stampMatrixA(a, 0, 1, 2, parameter, 0.001);
// Verify conductance stamping (1/100 = 0.01)
assertEquals(0.01, a[0][0], 1e-6);
assertEquals(0.01, a[1][1], 1e-6);
assertEquals(-0.01, a[0][1], 1e-6);
assertEquals(-0.01, a[1][0], 1e-6);
}
@Test
public void testCurrentCalculation() {
ResistorStamper stamper = new ResistorStamper();
double[] parameter = {50.0}; // 50 Ω
double current = stamper.calculateCurrent(10.0, 5.0, parameter, 0.001, 0.0);
assertEquals(0.1, current, 1e-6); // (10-5)/50 = 0.1 A
}
}
Coverage Thresholds¶
Core packages must maintain 60%+ instruction coverage: - circuit.matrix - circuit.netlist - circuit.simulation - control.calculators - math - datacontainer
Check CorePackageValidationTest for enforced boundaries—any Swing/AWT imports in these packages will fail the build.
Code Quality¶
Configuration¶
The project uses custom configurations for static analysis tools:
- SpotBugs: Default rules + 204 inline
@SuppressFBWarningsannotations — 0 bugs - PMD: Custom ruleset
pmd-ruleset.xml(quickstart rules, excludescom/intel/mkl/) - Checkstyle: Custom config
checkstyle.xml(150-char line length, relaxed naming for_prefixconvention)
Running Code Quality Checks¶
# SpotBugs (bug detection) — must remain at 0 bugs
mvn spotbugs:check
# Checkstyle (coding standards, custom config)
mvn checkstyle:check
# PMD (code smell detection, custom ruleset)
mvn pmd:check
# All checks
mvn verify
Common Issues¶
| Issue | Fix |
|---|---|
| Public mutable fields | Use @SuppressFBWarnings with justification if performance-critical |
| GUI imports in core | Move to separate *UI class or GUI package |
| Missing unit tests | Minimum 60% coverage on core packages |
| Unused parameters | Prefix with @SuppressWarnings("unused") or remove |
| Empty catch blocks | Add // intentionally empty comment or handle the exception |
| Unused private methods/fields | Remove them — dead code masks real issues |
Style Guide¶
- Naming:
camelCasefor variables,PascalCasefor classes - Member fields:
_camelCaseprefix convention (configured in Checkstyle) - Constants:
UPPER_SNAKE_CASE - Line length: 150 characters maximum (configured in Checkstyle)
- Methods: Prefer descriptive names (
calculateCurrentnotcalc) - Javadoc: Required for public classes/methods
- Access: Package-private by default, public only if necessary
Common Patterns¶
1. The Stamper Pattern¶
Used throughout the circuit domain. Each component type has a stamper that knows how to contribute to MNA matrices.
Pattern:
public class XyzStamper implements IMatrixStamper {
public void stampMatrixA(...) { /* Fill A */ }
public void stampVectorB(...) { /* Fill b */ }
public double calculateCurrent(...) { /* Compute I */ }
public double getAdmittanceWeight(...) { /* Return weight */ }
}
Benefits: Decouples component logic from solver
2. The Calculator Pattern¶
Used for control blocks. Calculators transform input signals to output signals via berechneYOUT().
Pattern:
public class XyzCalculator extends AbstractSingleInputSingleOutputCalculator {
public void berechneYOUT(final double deltaT) {
_outputSignal[0][0] = f(_inputSignal[0][0]);
}
}
Benefits: Simple state machine; easy to chain/compose
3. Signal Array Architecture¶
Signals are 2D arrays double[port][value]. Port 0 = first output, Port 1 = second, etc.
// Single output (most calculators)
_outputSignal[0][0] = result;
// Multiple outputs (e.g., dq to abc transformer)
_outputSignal[0][0] = phase_a; // Port 0
_outputSignal[1][0] = phase_b; // Port 1
_outputSignal[2][0] = phase_c; // Port 2
4. Registry Pattern¶
Components and stampers are registered dynamically:
StamperRegistry registry = StamperRegistry.createDefault();
registry.register(CircuitTyp.LK_CUSTOM, new MyStamper());
IMatrixStamper stamper = registry.getStamper(CircuitTyp.LK_CUSTOM);
Benefits: Easy to mock, test, and swap implementations
5. State Management Pattern¶
Stateful components track internal state and signal changes:
public class XyzCalculator extends AbstractSingleInputSingleOutputCalculator {
private double _state = 0.0;
private boolean _stateChanged = false;
@Override
public void berechneYOUT(final double deltaT) {
_state += /* update */;
}
public void updateState(double newValue) {
if (newValue != _state) {
_state = newValue;
_stateChanged = true;
}
}
public void resetStateChange() {
_stateChanged = false;
}
}
Troubleshooting¶
Build Fails: "No GUI imports in core packages"¶
Cause: You imported java.awt or javax.swing in a core package
Fix: Move GUI code to separate *UI class in allg/ or gui/ package
// Wrong
public class CalculatorUI extends JPanel { ... } // in calculators/
// Right
public class CalculatorUIPanel extends JPanel { ... } // in allg/ui/
Test Coverage Below 60%¶
Cause: New code not tested
Fix: Add unit tests
Stateful Component Not Converging¶
Cause: State changes not signaled properly
Fix: Ensure isStateChanged() returns true after state update
@Override
public void updateState(double vx, double vy, double current, double time) {
boolean oldState = _isOn;
_isOn = (current > THRESHOLD);
_stateChanged = (oldState != _isOn); // Signal change!
}
Calculator Output Always Zero¶
Cause: Input signal not connected
Fix: Initialize input signal array
Next Steps¶
- Read the source code: Browse
circuit/andcontrol/calculators/packages to understand patterns - Try a simple extension: Add a custom calculator (follow the gain/saturation examples)
- Run tests: Verify your code meets coverage and style standards
- Contribute: Submit a pull request with your changes
For architectural deep-dives, see: - .claude/journals/STRATEGIC_ROADMAP_DUAL_TRACK.md - Long-term vision - .claude/journals/CORE_API_BOUNDARY.md - GUI-free package documentation - .claude/journals/OPCODE_GUIDE.md - Detailed development reference