Skip to content

Commit 7075ae6

Browse files
committed
fix: skip saddle validation in fixed-point check
1 parent b1b78d2 commit 7075ae6

2 files changed

Lines changed: 40 additions & 15 deletions

File tree

docs/deployment.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ The widget automatically locates fixed points (equilibria) by intersecting nullc
186186
1. **Nullcline intersection** → candidate location
187187
2. **Newton–Raphson refinement** → precise `(x*, y*)`
188188
3. **Jacobian eigenvalues** at the fixed point → preliminary type
189-
4. **Dynamic validation**: a 3-second RK4 trajectory is launched from ` (x*+0.02, y*+0.02)` and compared against the eigenvalue prediction. If the two methods disagree (possible near bifurcations where numerical noise matters), the eigenvalue result is recomputed at the refined point and used as the final label.
189+
4. **Dynamic validation** (for non-saddles): a 3-second RK4 trajectory is launched from `(x*+0.02, y*+0.02)`.
190+
- **Stable / unstable points**: the trajectory must converge / diverge respectively. If it doesn't, the eigenvalue classification is recomputed at the refined point.
191+
- **Saddles are skipped**: a generic perturbation near a saddle always contains a component along the unstable manifold, so forward integration can only diverge — the test is structurally incapable of confirming (or refuting) a saddle. The eigenvalue-based "saddle" label (opposite real-part signs on a 2×2 Jacobian) is already unambiguous.
190192

191193
### Visual Legend
192194

src/tvb_phaseplane/static/widget.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13075,31 +13075,54 @@ function _quickRk4(f, y0, t0, tf, dt, params) {
1307513075

1307613076
/** Validate a fixed point by running a short trajectory from a perturbed IC.
1307713077
* Cross-checks the eigenvalue-based classification against actual dynamics.
13078-
* Does NOT display the validation trajectory — it is for classification only. */
13078+
* Does NOT display the validation trajectory — it is for classification only.
13079+
*
13080+
* IMPORTANT: Saddles are skipped. A generic perturbation near a saddle has
13081+
* a component along the unstable manifold, so forward integration always
13082+
* diverges; this makes the test useless for saddles. The eigenvalue-based
13083+
* "saddle" label (opposite signs on a 2×2 Jacobian) is already robust.
13084+
*/
1307913085
function validateFixedPoint(f, fp, params, classification) {
13086+
// Saddles: skip — forward integration from a random perturbation never
13087+
// converges to a saddle, so the test is structurally incapable of confirming
13088+
// the classification.
13089+
if (classification === "saddle") {
13090+
return classification;
13091+
}
13092+
1308013093
const eps = 0.02;
1308113094
const tEnd = 3.0;
1308213095
const dt = 0.01;
1308313096
const y0 = [fp[0] + eps, fp[1] + eps];
1308413097
const yEnd = _quickRk4(f, y0, 0, tEnd, dt, params);
1308513098

13086-
const distStart = Math.sqrt((y0[0] - fp[0]) ** 2 + (y0[1] - fp[1]) ** 2);
13087-
const distEnd = Math.sqrt((yEnd[0] - fp[0]) ** 2 + (yEnd[1] - fp[1]) ** 2);
13088-
13089-
const converges = distEnd < distStart * 0.5;
13090-
const diverges = distEnd > distStart * 2.0;
13091-
13092-
// Cross-check with eigenvalue classification
13093-
if (converges && !classification.startsWith("stable")) {
13099+
const distStart = Math.sqrt(
13100+
(y0[0] - fp[0]) ** 2 + (y0[1] - fp[1]) ** 2
13101+
);
13102+
const distEnd = Math.sqrt(
13103+
(yEnd[0] - fp[0]) ** 2 + (yEnd[1] - fp[1]) ** 2
13104+
);
13105+
13106+
// Stable node / focus: verify the trajectory actually converges
13107+
if (classification.startsWith("stable")) {
13108+
if (distEnd < distStart * 0.5) {
13109+
return classification; // confirmed
13110+
}
13111+
// Did not converge → recompute (numerical noise near bifurcation?)
1309413112
const J = jacobianND(f, [fp[0], fp[1]], params);
13095-
const ev = eigenvalues2x2(J);
13096-
return classifyFixedPoint(ev);
13113+
return classifyFixedPoint(eigenvalues2x2(J));
1309713114
}
13098-
if (diverges && !classification.startsWith("unstable")) {
13115+
13116+
// Unstable node / focus: verify the trajectory actually diverges
13117+
if (classification.startsWith("unstable")) {
13118+
if (distEnd > distStart * 2.0) {
13119+
return classification; // confirmed
13120+
}
13121+
// Did not diverge → recompute
1309913122
const J = jacobianND(f, [fp[0], fp[1]], params);
13100-
const ev = eigenvalues2x2(J);
13101-
return classifyFixedPoint(ev);
13123+
return classifyFixedPoint(eigenvalues2x2(J));
1310213124
}
13125+
1310313126
return classification;
1310413127
}
1310513128

0 commit comments

Comments
 (0)