✂️ Intro to slices

A witness set is how numerical algebraic geometry represents a positive-dimensional component of a variety – a curve, a surface, and so on. It is a triple:

  • a system (the equations whose solution set contains the component),

  • a generic linear slice (enough hyperplanes to cut the component down to points), and

  • the points where the component meets that slice.

The two headline invariants fall straight out: the dimension of the component is the number of hyperplanes in the slice, and the degree is the number of witness points.

The system and the points are ordinary objects you already know. The slice is the new one, and it carries a fair amount of semantics – so most of this tutorial is about slices.

The slice: a stack of linear forms

A Slice is a stack of linear forms \(M\,[x ; 1]\) – one row per form, the trailing column of each row holding that form’s constant term. You build one from exact coefficients with from_coefficients():

x, y = bertini.Variable('x'), bertini.Variable('y')

# two linear forms on (x, y):  2x + 3y + 1   and   x - y + 4
s = Slice.from_coefficients([[2, 3, 1], [1, -1, 4]], [x, y])

assert s.dimension() == 2        # two forms -> cuts a 2-dimensional component
assert s.num_variables() == 2

or generate a generic one (the usual case in practice – a witness set’s slice is random):

x0, x1, x2 = bertini.Variable('x0'), bertini.Variable('x1'), bertini.Variable('x2')
vg = bertini.VariableGroup([x0, x1, x2])

generic = Slice.random_complex(vg, 2)    # two random hyperplanes in 3-space
assert generic.dimension() == 2
assert generic.num_variables() == 3

Note

Coefficients must be exact (ints, fractions.Fraction, exact strings like '3/4', or bertini.multiprec values). A Python float is refused – its ~16 digits would cap the precision of the arbitrary-precision tree. See coefficient().

A slice is a sequence of forms – and the shape rules matter

This is the part with “lots of semantics running around,” so it is worth being precise. A slice behaves like a Python sequence of its linear forms, with the usual list semantics:

  • an integer index selects an element – the i-th form’s coefficient vector;

  • a slice [i:j] (or a list of indices) selects a sub-collection – a new (sub-)``Slice``;

  • the whole coefficient matrix is coefficients(), always 2-D.

# an ELEMENT: slice[i] is the i-th form's coefficient VECTOR (1-D), "a line is a vector"
v = np.asarray(s[0])
assert v.shape == (3,)
assert [complex(c).real for c in v] == [2.0, 3.0, 1.0]      # 2x + 3y + 1

# iterating yields the form vectors
assert len(list(s)) == 2

# a SUB-COLLECTION: slice[i:j] is a new Slice
first = s[:1]
assert first.dimension() == 1

# the whole matrix: always 2-D, (num_forms, num_variables + 1)
assert s.coefficients().shape == (2, 3)

The “always 2-D” is the important guarantee. The underlying binding library (eigenpy today) returns a one-row matrix to Python as a 1-D array – a data-dependent shape that silently breaks code written for the many-row case. This bites constantly in practice: a curve (dimension 1) has a single-form slice, so its coefficient matrix is exactly the one that would collapse.

bertini owns the shape so you do not have to think about it: coefficients() is always 2-D, even for one form, and the 1-D “vector” view is something you ask for by indexing an element (s[i]) – never something the form count hands you by surprise.

one_form = Slice.from_coefficients([[2, 3, 1]], [x, y])
assert one_form.coefficients().shape == (1, 3)     # NOT (3,) -- it never collapses
assert np.asarray(one_form[0]).shape == (3,)       # the vector view is explicit

(The “why” – and the list of accessors that still carry the raw hazard – is recorded in docs/adr/0033.)

A slice rides along with a system

A slice does not know about homogenization or patches; the system owns those. A slice is just linear forms, meant to be carried alongside a system. Two ways to put them together:

# as_system(): a standalone System of just the slice's forms
only_slice = s.as_system()
assert only_slice.num_functions() == 2

# add_to(): append the slice's forms to an existing system (over the same variables)
sphere = bertini.System()
sphere.add_variable_group(vg)
sphere.add_function(x0*x0 + x1*x1 + x2*x2 - 1)      # the unit sphere: a surface in 3-space
generic.add_to(sphere)
assert sphere.num_functions() == 3                  # 1 sphere equation + 2 slice forms

Note

add_to is homogenization-aware. If the system has already been homogenized, the slice’s constant term is folded onto the homogenizing variable (exactly as the system did to its own functions), so the appended forms stay consistent. You never homogenize the slice yourself.

The witness set

Now assemble the triple. You can build a witness set all at once, or incrementally – add points as you find them:

def pt(*entries):
    return np.array([mp.complex_mp(str(e)) for e in entries], dtype=mp.complex_mp)

sys = bertini.System()
sys.add_variable_group(vg)
sys.add_function(x0*x0 + x1*x1 + x2*x2 - 1)         # the sphere again (a 2-dim component)
slice2 = Slice.random_complex(vg, 2)

# all at once: points + slice + system
w = WitnessSetMultiplePrecision([pt(1, 0, 0), pt(0, 1, 0)], slice2, sys)
assert w.degree() == 2          # two witness points
assert w.dimension() == 2       # a surface
assert w.is_consistent()        # 3 variables - 1 equation == slice dimension 2

# or incrementally
w2 = WitnessSetMultiplePrecision()
w2.set_system(sys)
w2.set_slice(slice2)
w2.add_point(pt(1, 0, 0))
w2.add_point(pt(0, 1, 0))
assert w2.degree() == 2

A witness set prints a readable summary:

print(repr(w))
WitnessSet -- dimension 2, degree 2 (consistent)
  slice:  2 linear forms on 3 variables
  system: 1 function on 3 variables

Carrying it around, and emitting to Bertini 1

A witness set (and a slice) serializes, so you can pickle one, send it to another process, or checkpoint it – the deserialized system is restored ready to evaluate:

import pickle
w_again = pickle.loads(pickle.dumps(w))
assert w_again.degree() == 2
assert w_again.is_consistent()

You can also emit the witness (square) system – the system with the slice’s forms appended – as a Bertini 1 classic input file, for cross-validation against the original solver:

square = w.witness_system()          # system + slice, the square system to track
assert square.num_functions() == 3
text = w.to_classic_input()
assert 'CONFIG' in text and 'INPUT' in text

Building start systems from slices (regeneration)

Because a slice’s rows share their layout with a products-of-linears block, a list of slices drops straight into a product-of-linears start system – the bridge regeneration is built on. Each slice becomes one function that is the product of its hyperplanes:

start = bertini.System()
start.add_variable_group(bertini.VariableGroup([x, y]))
sa = Slice.from_coefficients([[1, 0, -1], [1, 0, 1]], [x, y])    # (x - 1)(x + 1)
sb = Slice.from_coefficients([[0, 1, -1], [0, 1, -2]], [x, y])   # (y - 1)(y - 2)
start.add_slices_as_products([sa, sb])
assert list(start.degrees()) == [2, 2]

Complete example

The whole tutorial as one runnable script – assemble nothing, just run it:

intro_to_slices.py
"""Intro to slices -- witness sets, slices, and start systems in Bertini 2.

Run:  python intro_to_slices.py
"""

import pickle

import numpy as np

import bertini
from bertini import Slice
from bertini import multiprec as mp
from bertini.nag_algorithm import WitnessSetMultiplePrecision


def build_slices():
    """The slice: a stack of linear forms."""
    x, y = bertini.Variable('x'), bertini.Variable('y')

    # two linear forms on (x, y):  2x + 3y + 1   and   x - y + 4
    s = Slice.from_coefficients([[2, 3, 1], [1, -1, 4]], [x, y])

    assert s.dimension() == 2        # two forms -> cuts a 2-dimensional component
    assert s.num_variables() == 2

    # or generate a generic one (the usual case -- a witness set's slice is random)
    x0, x1, x2 = bertini.Variable('x0'), bertini.Variable('x1'), bertini.Variable('x2')
    vg = bertini.VariableGroup([x0, x1, x2])

    generic = Slice.random_complex(vg, 2)    # two random hyperplanes in 3-space
    assert generic.dimension() == 2
    assert generic.num_variables() == 3

    return x, y, s, x0, x1, x2, vg, generic


def sequence_semantics(x, y, s):
    """A slice is a sequence of forms -- and the shape rules matter."""
    # an ELEMENT: slice[i] is the i-th form's coefficient VECTOR (1-D), "a line is a vector"
    v = np.asarray(s[0])
    assert v.shape == (3,)
    assert [complex(c).real for c in v] == [2.0, 3.0, 1.0]      # 2x + 3y + 1

    # iterating yields the form vectors
    assert len(list(s)) == 2

    # a SUB-COLLECTION: slice[i:j] is a new Slice
    first = s[:1]
    assert first.dimension() == 1

    # the whole matrix: always 2-D, (num_forms, num_variables + 1)
    assert s.coefficients().shape == (2, 3)

    # a single-form slice never collapses to 1-D
    one_form = Slice.from_coefficients([[2, 3, 1]], [x, y])
    assert one_form.coefficients().shape == (1, 3)     # NOT (3,) -- it never collapses
    assert np.asarray(one_form[0]).shape == (3,)       # the vector view is explicit


def slice_rides_along(s, x0, x1, x2, vg, generic):
    """A slice rides along with a system."""
    # as_system(): a standalone System of just the slice's forms
    only_slice = s.as_system()
    assert only_slice.num_functions() == 2

    # add_to(): append the slice's forms to an existing system (over the same variables)
    sphere = bertini.System()
    sphere.add_variable_group(vg)
    sphere.add_function(x0*x0 + x1*x1 + x2*x2 - 1)      # the unit sphere: a surface in 3-space
    generic.add_to(sphere)
    assert sphere.num_functions() == 3                  # 1 sphere equation + 2 slice forms


def build_witness_set(x0, x1, x2, vg):
    """The witness set: assemble the triple."""
    def pt(*entries):
        return np.array([mp.complex_mp(str(e)) for e in entries], dtype=mp.complex_mp)

    sys = bertini.System()
    sys.add_variable_group(vg)
    sys.add_function(x0*x0 + x1*x1 + x2*x2 - 1)         # the sphere again (a 2-dim component)
    slice2 = Slice.random_complex(vg, 2)

    # all at once: points + slice + system
    w = WitnessSetMultiplePrecision([pt(1, 0, 0), pt(0, 1, 0)], slice2, sys)
    assert w.degree() == 2          # two witness points
    assert w.dimension() == 2       # a surface
    assert w.is_consistent()        # 3 variables - 1 equation == slice dimension 2

    # or incrementally
    w2 = WitnessSetMultiplePrecision()
    w2.set_system(sys)
    w2.set_slice(slice2)
    w2.add_point(pt(1, 0, 0))
    w2.add_point(pt(0, 1, 0))
    assert w2.degree() == 2

    # a witness set prints a readable summary
    print(repr(w))

    return w


def serialize_and_emit(w):
    """Carrying it around, and emitting to Bertini 1."""
    w_again = pickle.loads(pickle.dumps(w))
    assert w_again.degree() == 2
    assert w_again.is_consistent()

    # emit the witness (square) system as a Bertini 1 classic input file
    square = w.witness_system()          # system + slice, the square system to track
    assert square.num_functions() == 3
    text = w.to_classic_input()
    assert 'CONFIG' in text and 'INPUT' in text


def start_from_slices(x, y):
    """Building start systems from slices (regeneration)."""
    start = bertini.System()
    start.add_variable_group(bertini.VariableGroup([x, y]))
    sa = Slice.from_coefficients([[1, 0, -1], [1, 0, 1]], [x, y])    # (x - 1)(x + 1)
    sb = Slice.from_coefficients([[0, 1, -1], [0, 1, -2]], [x, y])   # (y - 1)(y - 2)
    start.add_slices_as_products([sa, sb])
    assert list(start.degrees()) == [2, 2]


def main():
    x, y, s, x0, x1, x2, vg, generic = build_slices()
    sequence_semantics(x, y, s)
    slice_rides_along(s, x0, x1, x2, vg, generic)
    w = build_witness_set(x0, x1, x2, vg)
    serialize_and_emit(w)
    start_from_slices(x, y)


if __name__ == '__main__':
    main()