You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It is raised from solveIdentityMinusScaledProduct after an LU factorization of I - scale·(left·right) fails luNearSingular (wellposed.go:22-26), and reaches
callers through ConnectByName, LFT, SafeFeedback, and DiscretizeWithOpts.
At that point the failure is a rank property of the assembled feedthrough matrix.
The function receives no signal metadata, so nothing downstream can attribute the
loop to particular blocks or channels.
Problem
A consumer that compiles a user-authored block diagram through ConnectByName
can only report the failure generically. Process Lab currently surfaces:
flowsheet contains an unsolvable algebraic loop; add dynamics or change a
direct-feedthrough gain
which is accurate but names nothing, leaving the user to find the offending path
by hand on a sheet that may have dozens of blocks.
The obvious application-side workaround is wrong. Reporting the strongly
connected component of the wiring graph names a strict superset of the culprits:
the loop is over Q·D, so any cycle passing through an integrator or lag has a
zero block in D and is perfectly well-posed. Blaming the SCC would flag valid
dynamic feedback. Attributing correctly requires walking the cycle in D-space
with the same per-block feedthrough the library already assembled — which is
library-side knowledge, not caller-side.
Proposal
Introduce a typed error carrying the implicated signals, wrapping the existing
sentinel so errors.Is(err, ErrAlgebraicLoop) keeps working for every current
caller:
typeAlgebraicLoopErrorstruct {
Signals []string// names of signals participating in the feedthrough cycleConditionfloat64// reciprocal condition estimate of (I - Q·D)
}
func (e*AlgebraicLoopError) Error() string { ... }
func (e*AlgebraicLoopError) Unwrap() error { returnErrAlgebraicLoop }
ConnectByName holds the names; solveIdentityMinusScaledProduct holds the
matrix. Threading a name slice (or an index→name resolver) into the solve helper
is what connects them.
Conditioning
Related, and cheap once the above is in place: the guard is luNearSingular, not
exact singularity. A near-singular feedthrough loop — unity feedback with a gain
of 0.999, say — fails identically to a structurally impossible one, even though
the user's real problem is conditioning rather than topology. And a loop just on
the passing side of the threshold compiles and returns a numerically poor E
with no signal at all.
Reporting the condition estimate on the error covers both cases with the same
machinery, since the factorization is already in hand. It would also let callers
distinguish "this cannot work" from "this barely works".
Acceptance criteria
AlgebraicLoopError carries implicated signal names and a condition estimate
errors.Is(err, ErrAlgebraicLoop) holds for every path that returns it today
(connect.go:1367, connect.go:1521, lft.go:164, safe_feedback.go:111, delay.go:2155, delay.go:2165, convert.go:481)
Existing assertions in connect_test.go:2309, lft_test.go:355, delay_test.go:3298, architecture_backlog_test.go:202 pass unchanged
A test asserts the reported signal set for a known direct-feedthrough loop,
and asserts a dynamic feedback loop through an integrator is not reported
A test covers the near-singular case and asserts the condition estimate
distinguishes it from the structurally singular one
Non-goals
Automatically breaking loops or inserting unit delays.
Changing what is accepted or rejected. This is diagnostics only; the
well-posedness decision stays exactly where it is.
Found while reviewing consumer-side handling in jamestjsp/go-htmx PR #4, which
deliberately keeps the generic message until this exists.
Context
ErrAlgebraicLoopis a bare sentinel:It is raised from
solveIdentityMinusScaledProductafter an LU factorization ofI - scale·(left·right)failsluNearSingular(wellposed.go:22-26), and reachescallers through
ConnectByName,LFT,SafeFeedback, andDiscretizeWithOpts.At that point the failure is a rank property of the assembled feedthrough matrix.
The function receives no signal metadata, so nothing downstream can attribute the
loop to particular blocks or channels.
Problem
A consumer that compiles a user-authored block diagram through
ConnectByNamecan only report the failure generically. Process Lab currently surfaces:
which is accurate but names nothing, leaving the user to find the offending path
by hand on a sheet that may have dozens of blocks.
The obvious application-side workaround is wrong. Reporting the strongly
connected component of the wiring graph names a strict superset of the culprits:
the loop is over
Q·D, so any cycle passing through an integrator or lag has azero block in
Dand is perfectly well-posed. Blaming the SCC would flag validdynamic feedback. Attributing correctly requires walking the cycle in
D-spacewith the same per-block feedthrough the library already assembled — which is
library-side knowledge, not caller-side.
Proposal
Introduce a typed error carrying the implicated signals, wrapping the existing
sentinel so
errors.Is(err, ErrAlgebraicLoop)keeps working for every currentcaller:
ConnectByNameholds the names;solveIdentityMinusScaledProductholds thematrix. Threading a name slice (or an index→name resolver) into the solve helper
is what connects them.
Conditioning
Related, and cheap once the above is in place: the guard is
luNearSingular, notexact singularity. A near-singular feedthrough loop — unity feedback with a gain
of 0.999, say — fails identically to a structurally impossible one, even though
the user's real problem is conditioning rather than topology. And a loop just on
the passing side of the threshold compiles and returns a numerically poor
Ewith no signal at all.
Reporting the condition estimate on the error covers both cases with the same
machinery, since the factorization is already in hand. It would also let callers
distinguish "this cannot work" from "this barely works".
Acceptance criteria
AlgebraicLoopErrorcarries implicated signal names and a condition estimateerrors.Is(err, ErrAlgebraicLoop)holds for every path that returns it today(
connect.go:1367,connect.go:1521,lft.go:164,safe_feedback.go:111,delay.go:2155,delay.go:2165,convert.go:481)connect_test.go:2309,lft_test.go:355,delay_test.go:3298,architecture_backlog_test.go:202pass unchangedand asserts a dynamic feedback loop through an integrator is not reported
distinguishes it from the structurally singular one
Non-goals
well-posedness decision stays exactly where it is.
Found while reviewing consumer-side handling in
jamestjsp/go-htmxPR #4, whichdeliberately keeps the generic message until this exists.