Implement a bootstrap component
The experimental bootstrap component interfaces expose polynomial approximation, polynomial evaluation, linear-transform compilation and evaluation, and periodic reduction. Each component defines its coordinate, polynomial basis, tensor axes, arithmetic state, level/scale recurrence, mutation behavior, and numerical range.
Polynomial approximation
An approximator chooses coefficients but not the homomorphic multiplication DAG. Use ascending-degree coefficients and record the physical design interval:
class MyApproximator:
def approximate(self, function, *, domain=(-1.0, 1.0), name="polynomial"):
coefficients = fit_with_my_method(function, domain)
return bs.PolynomialApproximation(
basis="power",
coefficients=tuple(coefficients),
domain=domain,
name=name,
)2
3
4
5
6
7
8
9
The two built-in basis conventions are
and
If domain=(a, b) is physical coordinate
PolynomialApproximation.evaluate_plaintext() receives
Polynomial evaluation
Implement required_levels() and evaluate() to execute another multiplication DAG:
class MyEvaluator:
def required_levels(self, polynomial):
return my_depth(polynomial.coefficients)
def evaluate(
self,
engine,
ciphertext,
polynomial,
*,
relinearization_key=None,
):
return my_homomorphic_dag(
engine,
ciphertext,
polynomial.coefficients,
relinearization_key=relinearization_key,
)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
State the basis your evaluator accepts. The built-in evaluators consume a two-component coefficient-domain standard-RNS Q ciphertext with axes [component, *batch, limb, coefficient] and exact active prime_ids. Their ciphertext products require a relinearization key and perform
coefficient/standard/Q/two components
-> NTT/Montgomery/Q/two components
-> NTT/Montgomery/Q/three components
-> coefficient/standard/Q/two components
-> drop leading Q row and reinterpret at default_scale2
3
4
5
A custom evaluator must report whether it follows that private fixed-scale policy or preserves the core actual scale required_levels(polynomial) == d unless every execution path advances by exactly relinearization_key=None only on an execution path whose polynomial DAG contains no ciphertext product, such as a constant or linear built-in polynomial.
Linear-transform compilation
A compiler returns immutable stages. A stage stores whatever numerical data the matching evaluator understands:
from dataclasses import dataclass
@dataclass(frozen=True)
class SparseStage:
slots: int
matrix: object
def reference(self, values):
return self.matrix @ values
class SparseCompiler:
def compile(self, *, slots, direction, generator, scale=1.0):
matrix = synthesize_sparse_map(slots, direction, generator, scale)
return (SparseStage(slots, matrix),)2
3
4
5
6
7
8
9
10
11
12
13
14
reference() is a plaintext oracle. Document its accepted axes and whether its input is a raw physical coordinate or a normalized coordinate. The built-in radix-2 reference uses exact shape [slot] and the convention
The compiler's scale argument multiplies numerical matrix values. It is not a CKKS metadata scale.
Linear-transform evaluation
class SparseEvaluator:
def required_rotation_offsets(self, transform):
return tuple(rotations_used_by(transform.matrix))
def required_levels(self, transform):
return 1
def evaluate(
self,
engine,
ciphertext,
transform,
*,
rotation_keys,
rotate,
encode_diagonal,
):
return evaluate_sparse_map(
engine,
ciphertext,
transform.matrix,
rotation_keys=rotation_keys,
rotate=rotate,
encode=encode_diagonal,
)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
The callbacks provide rotation-key decomposition and diagonal encoding/cache policy. A built-in diagonal stage encodes an unbatched [limb, ntt_index] Montgomery plaintext at scale
The output remains two-component coefficient-domain standard RNS over Q, with unchanged batch axes and the leading prime_ids row removed. If a custom evaluator uses another recurrence or state transition, report the differing recurrence or transition.
For a cyclic-diagonal map
direct and BSGS evaluation are alternative schedules, not alternative maps. A BSGS implementation should test the identity
Periodic reduction (modular_reduction)
A reduction component must distinguish raw and normalized coordinates. Let input_bound be
A custom component protocol can be as small as:
class MyReduction:
input_bound = 1024
fuse_input_normalization = True
requires_relinearization = False
@property
def required_levels(self):
return 6
@property
def fused_input_divisor(self):
return float(self.input_bound) if self.fuse_input_normalization else 1.0
def reference(self, normalized_values):
return my_plaintext_periodic_oracle(normalized_values)
def evaluate(
self,
engine,
ciphertext,
*,
relinearization_key=None,
conjugation_key=None,
):
del relinearization_key, conjugation_key
# With fusion, ciphertext already represents x = r / input_bound.
return my_periodic_reduction_without_ciphertext_products(
engine, ciphertext
)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
The component interface specification must state:
- whether
reference()accepts or ; - whether
evaluate()accepts or under each fusion setting; - the raw admissible interval and output target;
- the polynomial basis and design interval;
- exact
required_levelsand output actual scale; - whether
requires_relinearizationis true because evaluation performs a ciphertext-ciphertext product; - whether ciphertext products or conjugation consume the supplied primitive keys;
- output level, component count, domain, basis, residue representation, and
prime_ids; - whether execution mutates or aliases an input.
FullSlotBootstrap expects fused_input_divisor to be the numerical factor folded into CoeffsToSlots. Returning evaluate() method receives keyword-only relinearization_key and conjugation_key values. A custom reduction that does not multiply ciphertexts supports relinearization_key=None; built-in reductions reject None. The full-slot topology always has a conjugation key for branch splitting, so reducers may use that same primitive without a second key declaration.
Full-slot integration requirements
A component that is inserted into FullSlotBootstrap participates in this state sequence:
The callable calculates
Its final scale is the actual SlotsToCoeffs recurrence, not necessarily default_scale. A component must not hide a level, scale reinterpretation, basis extension, NTT transition, or range normalization from its declared state-transition specification.
Test the behavior that matters
Test components independently before inserting them into a full bootstrap:
- compare
reference()with the intended map on the documented coordinate; - test raw-to-normalized equivalence using
; - compare encrypted component output with the plaintext oracle;
- assert output level advancement and actual scale recurrence;
- assert component, batch, limb, and coefficient/NTT axes;
- assert domain, basis, residue representation, and exact
prime_ids; - compare direct and BSGS decoded outputs for the same mathematical map;
- run full-slot end-to-end refresh across several seeds and admissible ranges;
- measure the application's actual raw branch range and error distribution.
Do not weaken a tolerance to accommodate unexplained error. First determine whether the implementation changed, the coordinate convention is wrong, or the original error model was unsound. No identity payload or complete-pipeline framework is required for these tests.