Auxiliary coordinates¶
Some systems carry coordinates that exist for the construction rather than for the answer. A
critical-point system in null-vector form is f; v^T M; patch, where v needs its own patch
or the system would admit the zero null vector. That patch fixes a normalization, so v’s
direction is the meaningful object and its magnitude is whatever the patch made it.
Bertini 2 judges a point by its largest coordinate. It does this three times: the tracker truncates
a path whose point exceeds path_truncation_threshold, the endgame abandons one that exceeds
Security.max_norm, and the solver classifies an endpoint as at infinity when it exceeds
endpoint_finite_threshold. If the largest coordinate belongs to a block like v, all three
are reading a number nobody really cares about. No threshold is the right one, because the quantity is not
governed.
The fix is to say which coordinates the question is about.
Note
This part of the system is motivated by the real cellular decomposition, which uses nullvectors a lot to compute critical points. And the nullvectors’ coordinates often trip the loss of a solution because the nullvector has large coordinates, even though the critical point doesn’t. So, I implemented this, so that I can say to the tracker “i don’t care about how large these coordinates are”. Perhaps you also find it useful?
Declaring variables as auxiliary¶
A system is told which of its coordinates are auxiliary, by variable group or by index:
import numpy as np
import bertini as pb
x, v = pb.Variable('x'), pb.Variable('v')
sys = pb.System()
sys.add_variable_group(pb.VariableGroup([x]))
sys.add_variable_group(pb.VariableGroup([v]))
sys.add_function(x*x - 1)
sys.add_function(v - 1)
point = np.array([complex(1e3), complex(1e9)], dtype=complex)
print(sys.is_finite(point, 1e5))
sys.set_auxiliary_variable_groups([1])
print(sys.is_finite(point, 1e5))
False
True
The point is the same in both calls. In the first, v = 1e9 decides the verdict. In the
second, the system has been told that v is not what finiteness is about, so the verdict rests
on x = 1e3.
set_auxiliary_coordinates takes indices instead of groups, for a system whose grouping does
not separate the two kinds of coordinate. Indices are into a point in your own coordinates, in
variable-group order, so they still mean the same thing after the solver homogenizes and patches.
The two lists are unioned; either can be emptied by passing [].
Solving with auxiliaries¶
{x^2 - 1, v - 100} has the solutions (1, 100) and (-1, 100). With the finiteness
threshold at 10, v puts both of them past it, and the solutions are labeled as
infinite. The problem can be even worse, if the tracker would truncate! On the other hand
if we set the v’s as aux, then the solutions get labeled as finite.
I know this example is contrived, but it’s the simplest possible to illustrate what I mean.
def solve_it(auxiliary):
s = pb.System()
s.add_variable_group(pb.VariableGroup([x, v]))
s.add_function(x*x - 1)
s.add_function(v - 100)
if auxiliary:
s.set_auxiliary_coordinates([1])
solver = pb.nag_algorithm.ZeroDimSolver(s)
solver.update(endpoint_finite_threshold=10)
solver.solve()
md = solver.solution_metadata()
return sum(1 for m in md if m.is_finite), sum(1 for m in md if m.is_real)
print(solve_it(auxiliary=False))
print(solve_it(auxiliary=True))
(0, 0)
(2, 2)
Both runs track the same two paths to the same two points, and both report Success.
Without the aux declaration, the solver calls two ordinary roots infinite, and
calls them complex as well, because realness is measured over the same coordinates.
More notes on aux vars¶
The post-processor vs during-path properties¶
Classification happens after the solve, so a caller who disagrees with it can always compute their own. Truncation happens during tracking. A path the tracker abandoned partway, because a coordinate that was never the question grew large, is simply not there afterwards, and no post-processing recovers it. That is the reason this belongs to the system rather than to a setting applied at the end: the same declaration reaches the tracker and the endgame, which is where it matters most.
What aux does not mean¶
Bertini attaches no further meaning to the word. An auxiliary coordinate is tracked like any other, is stored in the solution, is written to records, and is returned to you. It is excluded from two questions and nothing else.
Marking every coordinate auxiliary is refused. A point with nothing left to judge would be finite and real unconditionally, which is not a verdict worth rendering.
aux’s and the recording system¶
Two systems that differ only in which coordinates are auxiliary have different content digests:
plain = pb.System()
plain.add_variable_group(pb.VariableGroup([x, v]))
plain.add_function(x*x - 1)
plain.add_function(v - 100)
declared = pb.System.from_canonical(plain.canonical_encoding())
declared.set_auxiliary_coordinates([1])
print(plain.content_digest() == declared.content_digest())
False
This is deliberate. The tracker truncates on the coordinates that are not auxiliary, so the two systems do not compute the same thing, and a records directory must not answer one with the other. Adding or removing a declaration makes a new ask, which is recomputed rather than recalled.
See also
⚙️ Configuration reference for the thresholds named here, and 🪢 Crossed paths: when two paths (accidentally) become one for the other per-path verdict a solve can render.