Skip to content

Commit be78635

Browse files
esaruohoclaude
andcommitted
Add voltage compliance to current source (pfalstad#241 redesign)
Adds an optional compliance voltage to CurrentElm. The redesign addresses both pieces of @pfalstad feedback on PR pfalstad#241: - No checkbox. Default maxVoltage = 0 means unlimited (ideal current source); a positive value enables compliance. The dialog field is labelled "Max Voltage (V, 0=unlimited)". - Convergence with switches open. The original implementation hard-stepped between "stamp ideal source" and "stamp open" once |vd| crossed maxVoltage. Newton-Raphson then ping-ponged between the two stamps with no fixed point, especially with high-impedance loads (open switches, large resistors). Replaced with a smooth tanh-shaped saturation: i(vd) = currentValue * 0.5 * (1 - tanh((|vd| - maxVoltage)/vt)) with vt = 5% of maxVoltage. The Norton companion model stamps the exact analytic Jacobian (di/dvd = -currentValue * 0.5 * sech^2(arg) / vt * sign(vd)) plus a small gmin so the matrix never goes singular when sech^2 is near zero. Result: the source rolls off smoothly to zero current as |vd| approaches maxVoltage rather than slamming between full-on and full-off, which is what was killing convergence in the test circuit @pfalstad shared on pfalstad#241. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7cf753f commit be78635

File tree

1 file changed

+70
-12
lines changed

1 file changed

+70
-12
lines changed

src/com/lushprojects/circuitjs1/client/CurrentElm.java

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
/*
1+
/*
22
Copyright (C) Paul Falstad and Iain Sharp
3-
3+
44
This file is part of CircuitJS1.
55
66
CircuitJS1 is free software: you can redistribute it and/or modify
@@ -24,35 +24,49 @@
2424

2525
class CurrentElm extends CircuitElm {
2626
double currentValue;
27+
// Compliance voltage. 0 = unlimited (ideal current source).
28+
double maxVoltage;
29+
double lastVoltDiff;
2730
boolean broken;
2831
public CurrentElm(int xx, int yy) {
2932
super(xx, yy);
3033
currentValue = .01;
34+
maxVoltage = 0;
3135
}
3236
public CurrentElm(int xa, int ya, int xb, int yb, int f,
3337
StringTokenizer st) {
3438
super(xa, ya, xb, yb, f);
3539
try {
3640
currentValue = new Double(st.nextToken()).doubleValue();
37-
} catch (Exception e) {
41+
maxVoltage = new Double(st.nextToken()).doubleValue();
42+
} catch (Exception e) {}
43+
if (currentValue == 0)
3844
currentValue = .01;
39-
}
45+
}
46+
boolean isVoltageLimited() { return maxVoltage > 0; }
47+
boolean nonLinear() { return isVoltageLimited(); }
48+
void reset() {
49+
super.reset();
50+
lastVoltDiff = 0;
4051
}
4152
String dump() {
42-
return super.dump() + " " + currentValue;
53+
return super.dump() + " " + currentValue + " " + maxVoltage;
4354
}
4455

4556
void dumpXml(Document doc, Element elem) {
4657
super.dumpXml(doc, elem);
4758
XMLSerializer.dumpAttr(elem, "cu", currentValue);
59+
if (maxVoltage > 0)
60+
XMLSerializer.dumpAttr(elem, "mv", maxVoltage);
4861
}
4962

5063
void undumpXml(XMLDeserializer xml) {
5164
super.undumpXml(xml);
5265
currentValue = xml.parseDoubleAttr("cu", currentValue);
66+
maxVoltage = xml.parseDoubleAttr("mv", 0);
5367
}
5468
int getDumpType() { return 'i'; }
55-
69+
5670
Polygon arrow;
5771
Point ashaft1, ashaft2, center;
5872
void setPoints() {
@@ -69,7 +83,7 @@ void draw(Graphics g) {
6983
draw2Leads(g);
7084
setVoltageColor(g, (volts[0]+volts[1])/2);
7185
setPowerColor(g, false);
72-
86+
7387
drawThickCircle(g, center.x, center.y, cr);
7488
drawThickLine(g, ashaft1, ashaft2);
7589

@@ -83,37 +97,81 @@ void draw(Graphics g) {
8397
}
8498
drawPosts(g);
8599
}
86-
100+
87101
// analyzeCircuit determines if current source has a path or if it's broken
88102
void setBroken(boolean b) {
89103
broken = b;
90104
}
91-
105+
92106
// we defer stamping current sources until we can tell if they have a current path or not
93107
void stamp() {
94108
if (broken) {
95109
// no current path; stamping a current source would cause a matrix error.
96110
sim.stampResistor(nodes[0], nodes[1], 1e8);
97111
current = 0;
112+
} else if (isVoltageLimited()) {
113+
// nonlinear; doStep() handles the smooth-saturation companion model
114+
sim.stampNonLinear(nodes[0]);
115+
sim.stampNonLinear(nodes[1]);
98116
} else {
99-
// ok to stamp a current source
117+
// ideal current source
100118
sim.stampCurrentSource(nodes[0], nodes[1], currentValue);
101119
current = currentValue;
102120
}
103121
}
104-
122+
123+
// Smooth voltage compliance via tanh-shaped saturation. Newton-Raphson
124+
// gets an exact Jacobian so it converges even when the load is open
125+
// (all switches off): as |vd| approaches maxVoltage, the source rolls off
126+
// to zero current rather than slamming between full-on and full-off.
127+
void doStep() {
128+
if (broken || !isVoltageLimited())
129+
return;
130+
double vd = volts[1] - volts[0];
131+
if (Math.abs(lastVoltDiff - vd) > 0.01)
132+
sim.converged = false;
133+
lastVoltDiff = vd;
134+
135+
double absVd = Math.abs(vd);
136+
double signVd = (vd >= 0) ? 1.0 : -1.0;
137+
double vt = Math.max(maxVoltage * 0.05, 1e-6);
138+
139+
// i(vd) = currentValue * 0.5 * (1 - tanh((|vd| - maxVoltage)/vt))
140+
double arg = (absVd - maxVoltage) / vt;
141+
double tanhArg = Math.tanh(arg);
142+
double i = currentValue * 0.5 * (1.0 - tanhArg);
143+
// di/dvd = -currentValue * 0.5 * sech^2(arg) / vt * sign(vd)
144+
double sech2 = 1.0 - tanhArg * tanhArg;
145+
double g = -currentValue * 0.5 * sech2 / vt * signVd;
146+
147+
// Norton companion: parallel resistor (1/|g|) + adjusted current source.
148+
// Add gmin so a singular matrix is avoided when sech^2 is near zero
149+
// (well inside or well outside compliance).
150+
double absG = Math.abs(g) + 1e-9;
151+
sim.stampResistor(nodes[0], nodes[1], 1.0 / absG);
152+
sim.stampCurrentSource(nodes[0], nodes[1], i - g * vd);
153+
current = i;
154+
}
155+
105156
public EditInfo getEditInfo(int n) {
106157
if (n == 0)
107158
return new EditInfo("Current (A)", currentValue, 0, .1);
159+
if (n == 1)
160+
return new EditInfo("Max Voltage (V, 0=unlimited)", maxVoltage, 0, 0);
108161
return null;
109162
}
110163
public void setEditValue(int n, EditInfo ei) {
111-
currentValue = ei.value;
164+
if (n == 0)
165+
currentValue = ei.value;
166+
if (n == 1 && ei.value >= 0)
167+
maxVoltage = ei.value;
112168
}
113169
void getInfo(String arr[]) {
114170
arr[0] = "current source";
115171
int i = getBasicInfo(arr);
116172
arr[i++] = "P = " + getUnitText(getPower(), "W");
173+
if (isVoltageLimited())
174+
arr[i++] = "Vmax = " + getVoltageText(maxVoltage);
117175
}
118176
double getVoltageDiff() {
119177
return volts[1] - volts[0];

0 commit comments

Comments
 (0)