fhelium.experimental.bootstrap
CKKS bootstrap components and full-slot bootstrap execution.
The package exposes independently replaceable polynomial, linear-transform, and modular-reduction mechanisms plus one engine-bound callable full-slot composition. Preconfigured constructors live in fhelium.experimental.bootstrap.presets.
BalancedPowerEvaluator
class View source
BalancedPowerEvaluator(skip_near_zero: float = 0.0)Evaluate a power series through a shared balanced product tree.
To construct
evaluate consumes a two-component coefficient-domain, standard-RNS Q ciphertext representing the basis coordinate [component, *batch, limb, coefficient]; all batch members share level, scale, and exact prime_ids. The method is functional. Each ciphertext product converts operands to NTT/Montgomery form, multiplies to three components, relinearizes back to two coefficient-domain standard components, drops one leading Q row, and applies the bootstrap's explicit default-scale reinterpretation. The final result is coefficient-domain standard RNS over Q at ciphertext.level + required_levels(polynomial), with the corresponding prime_ids, unchanged batch shape, two components, and actual default scale.
Attributes
| Name | Type | Default/value |
|---|---|---|
skip_near_zero | float | 0.0 |
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return exact multiplying operations executed by evaluate.
Coefficient multiplication counts only active nonconstant terms. Alignment multiplication counts every multiply-by-one level advance, including advances used inside the shared power tree and before term addition. Additions and plaintext encoding are not multiplications.
required_levels
method
def required_levels(polynomial: PolynomialApproximation) -> int: ...Count the deepest balanced-product path and coefficient product.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, polynomial: PolynomialApproximation, *, relinearization_key: RelinearizationKey | None=None) -> Ciphertext: ...Evaluate
Coefficients with magnitude at or below skip_near_zero are omitted. A constant-only polynomial still consumes one level by multiplying the input by zero; this keeps the execution behavior equal to the declared one-level cost. Inputs are not mutated and output storage does not alias an input.
BinaryDecompositionChebyshevEvaluator
class View source
BinaryDecompositionChebyshevEvaluator(skip_near_zero: float = 0.0)Evaluate a Chebyshev series through shared doubling identities.
The evaluator builds only basis elements required by nonzero terms. It recursively uses
caching every required
The coordinate, tensor axes, arithmetic-state preconditions, functional behavior, per-product transitions, output level, active prime_ids, and explicit default-scale policy match BalancedPowerEvaluator; only the polynomial basis and multiplication DAG differ.
Attributes
| Name | Type | Default/value |
|---|---|---|
skip_near_zero | float | 0.0 |
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return exact multiplying operations executed by evaluate.
Alignment includes operand advancement within odd recurrences, advancement of the original
required_levels
method
def required_levels(polynomial: PolynomialApproximation) -> int: ...Count the deepest required recurrence plus coefficient product.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, polynomial: PolynomialApproximation, *, relinearization_key: RelinearizationKey | None=None) -> Ciphertext: ...Build required
Terms at or below skip_near_zero are omitted. As in the power evaluator, the constant-only case deliberately consumes one level so its execution agrees with required_levels. The method is functional and returns a two-component coefficient-domain standard-RNS Q value at actual default scale.
ChebyshevInterpolator
class View source
ChebyshevInterpolator(degree: int, error_samples: int = 8193)Fit a degree-limited Chebyshev series at first-kind nodes.
degree controls both the number of interpolation nodes and the highest returned term error_samples controls only the dense grid used to report max_error; that sampled value is not a proof of the uniform error between grid points.
Attributes
| Name | Type | Default/value |
|---|---|---|
degree | int | |
error_samples | int | 8193 |
approximate
method
def approximate(function: Callable[[np.ndarray], np.ndarray], *, domain: tuple[float, float]=(-1.0, 1.0), name: str='polynomial') -> PolynomialApproximation: ...Interpolate after mapping physical
If domain=(a, b), first-kind nodes
Coefficients in the returned object are functions of normalized
max_error is measured on error_samples equally spaced normalized coordinates after fitting. Approximation runs on CPU binary64/complex128 arrays and returns no encrypted tensor.
CosineDoubleAngleReduction
class View source
CosineDoubleAngleReduction(input_bound: int, double_angle_iterations: int, approximator: Any, evaluator: Any, fuse_input_normalization: bool = False, maximum_plaintext_error: float = 0.001)Reduce one real branch with a cosine seed and double-angle chain.
input_bound is double_angle_iterations, the seed is
and iteration
Thus
reference(values) always takes normalized evaluate(...) takes raw fuse_input_normalization=False, dividing by
The homomorphic input is a two-component coefficient-domain, standard-RNS Q ciphertext with payload axes [component, *batch, limb, coefficient], exact active prime_ids, and actual scale near engine.config.default_scale. Evaluation is functional. Each multiplication returns to coefficient-domain standard RNS, drops one leading Q row on rescale, and is explicitly reinterpreted at the default scale by the bootstrap's private fixed-scale policy. The output has the same batch and component axes, Q basis, and context; its level advances by required_levels, its limb axis contains the corresponding suffix of prime_ids, and its actual scale is the default scale.
More iterations reduce the seed frequency but each iteration costs one ciphertext multiplication, relinearization, and rescale level. Neither the class nor evaluate measures the encrypted branch range; the caller must establish
Attributes
| Name | Type | Default/value |
|---|---|---|
requires_relinearization | True | |
input_bound | int | |
double_angle_iterations | int | |
approximator | Any | |
evaluator | Any | |
fuse_input_normalization | bool | False |
maximum_plaintext_error | float | 0.001 |
polynomial
property
polynomial: PolynomialApproximationFit the low-frequency seed in normalized coordinate
For double_angle_iterations, the returned Chebyshev or power series approximates
Its coefficient basis is selected by approximator; coefficients use that basis's ascending-degree convention.
approximation_error
property
approximation_error: floatSample
The grid spans normalized
fused_input_divisor
property
fused_input_divisor: floatReturn
required_levels
property
required_levels: intCount explicit division, polynomial depth, and recurrence depth.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, *, relinearization_key: RelinearizationKey | None, conjugation_key: ConjugationKey | None=None) -> Ciphertext: ...Evaluate
With non-fused normalization the input represents raw ciphertext.level + required_levels. Input storage is not mutated or aliased by the result.
reference
method
def reference(values: np.ndarray) -> np.ndarray: ...Evaluate the plaintext oracle on normalized coordinates
values may have any NumPy-broadcastable shape and that shape is preserved. Unlike non-fused evaluate, this method never divides by values / input_bound. The target is
DiagonalBSGSEvaluator
class View source
DiagonalBSGSEvaluator(baby_step: int, hoist_baby_rotations: bool = True)Evaluate the same diagonal map with a BSGS rotation schedule.
An offset baby_step. Baby rotations of the input are shared across giant groups. Each group's diagonals are shifted to compensate for the final giant rotation, its plaintext products are accumulated and rescaled, and then the group result is giant-rotated into place.
Algebraically, each term is unchanged because
Thus direct and BSGS evaluators implement the same map and level/scale/state transition; different grouping and CKKS rounding need not produce bit-identical residues. hoist_baby_rotations uses engine.rotate_many_with_keys only when exact baby keys are available. Compact power-of-two inventories compose rotations through the private key-aware evaluation helper.
Attributes
| Name | Type | Default/value |
|---|---|---|
baby_step | int | |
hoist_baby_rotations | bool | True |
required_levels
method
def required_levels(transform: Any) -> int: ...Return the single rescale consumed by one BSGS stage.
required_rotation_offsets
method
def required_rotation_offsets(transform: Any) -> tuple[int, ...]: ...Return the union of nonzero baby and giant rotations.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, transform: Any, *, rotation_keys: RotationKeySet, rotate: Callable[[Ciphertext, int], Ciphertext], encode_diagonal: Callable[..., Plaintext]) -> Ciphertext: ...Execute shared baby rotations, group sums, and giant rotations.
Each giant-group accumulator is rescaled before its giant rotation, so all group results have common level and actual scale
The input and output tensor/state requirements are identical to DirectDiagonalEvaluator; evaluation is functional.
DiagonalLinearTransform
class View source
DiagonalLinearTransform(diagonals: Mapping[int, ArrayLike], slots: int, name: str = 'diagonal_linear_transform')An immutable cyclic-diagonal linear map over packed CKKS slots.
The map is
Each stored diagonal is a CPU complex128 NumPy vector with shape [slot]; reference accepts and returns the same one-dimensional shape. This object contains no choice of execution algorithm. The matching evaluator independently decides whether to use direct diagonals, BSGS, hoisting, distribution, or a user implementation. Offsets are cyclic modulo -1 and slots - 1 are combined by normalized_diagonals.
Attributes
| Name | Type | Default/value |
|---|---|---|
diagonals | Mapping[int, ArrayLike] | |
slots | int | |
name | str | 'diagonal_linear_transform' |
normalized_diagonals
method
def normalized_diagonals() -> dict[int, np.ndarray]: ...Map every offset to
The stored arrays remain immutable. A new mapping is returned because two input offsets can normalize to the same cyclic key and must then be added elementwise. Returned vectors retain shape [slot].
reference
method
def reference(values: ArrayLike) -> np.ndarray: ...Apply
values must have exact shape [slot]. The returned CPU complex128 array has shape [slot]. This plaintext oracle does not encode, rescale, consume levels, or model CKKS error.
DirectDiagonalEvaluator
class View source
DirectDiagonalEvaluator()Evaluate each cyclic diagonal independently, then rescale once.
For every nonzero offset this strategy rotates the input, multiplies it by the corresponding encoded diagonal, and adds the product to an accumulator. All products have pending scale
The input is a two-component coefficient-domain standard-RNS Q ciphertext with data axes [component, *batch, limb, coefficient], ring extent prime_ids tuple. If the leading active prime is
and Q prime_ids with the leading row removed. The output remains in coefficient domain with standard residues; temporary diagonal plaintexts are NTT-domain Montgomery RNS. The result does not alias an input.
required_levels
method
def required_levels(transform: Any) -> int: ...Return the single rescale consumed by one diagonal stage.
required_rotation_offsets
method
def required_rotation_offsets(transform: Any) -> tuple[int, ...]: ...Return direct non-zero diagonal offsets.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, transform: Any, *, rotation_keys: RotationKeySet, rotate: Callable[[Ciphertext, int], Ciphertext], encode_diagonal: Callable[..., Plaintext]) -> Ciphertext: ...Apply
The input must match the engine's slot count. For offset zero the input is reused directly; every other term requests one rotation through the supplied rotation-key strategy. Each diagonal is encoded at the input level, multiplied into its rotated ciphertext, and accumulated at pending scale. A single final rescale advances the output by one level.
Raises
TypeError: Iftransformuses another stage representation.ValueError: If slot count or diagonal content is invalid.
ExponentialSquaringReduction
class View source
ExponentialSquaringReduction(input_bound: int, degree: int, evaluator: Any = BalancedPowerEvaluator(), fuse_input_normalization: bool = False)Reduce one real branch through a truncated exponential and squaring.
Let input_bound,
Repeated squaring computes
reference(values) always consumes normalized evaluate(...) consumes raw fuse_input_normalization=False; with fusion enabled its caller must provide
The ciphertext state, axes, functional behavior, level transition, and private default-scale reinterpretation are the same as for CosineDoubleAngleReduction. This strategy additionally requires a conjugation key. It returns a two-component coefficient-domain, standard-RNS Q ciphertext at ciphertext.level + required_levels, with the corresponding active prime_ids and actual default scale.
Attributes
| Name | Type | Default/value |
|---|---|---|
requires_relinearization | True | |
input_bound | int | |
degree | int | |
evaluator | Any | BalancedPowerEvaluator() |
fuse_input_normalization | bool | False |
polynomial
property
polynomial: PolynomialApproximationReturn ascending power coefficients for
Entry
squaring_iterations
property
squaring_iterations: intReturn
fused_input_divisor
property
fused_input_divisor: floatReturn
required_levels
property
required_levels: intCount normalization, polynomial DAG, squarings, and sine scaling.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, *, relinearization_key: RelinearizationKey | None, conjugation_key: ConjugationKey | None=None) -> Ciphertext: ...Evaluate
Non-fused evaluation first maps raw
reference
method
def reference(values: np.ndarray) -> np.ndarray: ...Evaluate the plaintext oracle on normalized coordinates
The input shape is preserved. This method never divides by values / input_bound. It models polynomial truncation and repeated squaring but not CKKS error.
FullSlotBootstrap
class View source
FullSlotBootstrap(engine: CkksEngine, *, coeffs_to_slots_compiler: Any, coeffs_to_slots_evaluator: Any, modular_reduction: Any, slots_to_coeffs_compiler: Any, slots_to_coeffs_evaluator: Any, modulus_raise_target_level: int=0, retain_diagonals: bool=False)Compiled full-slot refresh callable with replaceable components.
Construction binds transform compilers/evaluators and modular reduction to one engine. Calling the object executes the visible full-slot algorithm with one validated evaluator-only key inventory.
Let engine.config.default_scale, input_bound and let fused_input_divisor, equal to
If
The encrypted transform and explicit
the idealized nonlinear and inverse-transform portion is
Polynomial approximation, CKKS arithmetic, key switching, and scale reinterpretation perturb this idealized map. The caller must establish the reducer's raw-coordinate precondition
required_rotations
property
required_rotations: tuple[int, ...]Return canonical signed
key_steps
method
def key_steps(strategy: str='exact') -> tuple[int, ...]: ...Return the rotation-key inventory for one composition strategy.
exact returns every logical transform step as a direct key. power_of_two returns the deduplicated signed-power steps whose compositions cover those transforms. The latter therefore describes actual inventory entries, not the original transform offsets.
Raises
ValueError: Ifstrategyis notexactorpower_of_two.
evaluation_key_requirements
method
def evaluation_key_requirements(rotation_strategy: str='power_of_two') -> EvaluationKeyRequirements: ...Return all evaluator capabilities for one rotation strategy.
Rotation steps come from key_steps. Conjugation is always required by full-slot reconstruction; relinearization is required only when the selected modular reduction declares ciphertext products. The result contains no key tensors or key-generation policy.
create_rotation_keys
method
def create_rotation_keys(secret_key: SecretKey, *, rotation_strategy: str='power_of_two') -> RotationKeySet: ...Generate only the selected bootstrap rotation-key inventory.
secret_key is consumed by primitive engine key generation and is not stored in the returned set. Relinearization and conjugation keys are intentionally not created here; applications construct those separate capabilities and assemble an EvaluationKeySet.
cached_diagonal_bytes
property
cached_diagonal_bytes: intReturn encoded diagonal tensor bytes retained by this evaluator.
clear_cache
method
def clear_cache() -> None: ...Release encoded diagonals and non-semantic arithmetic tables.
HornerPowerEvaluator
class View source
HornerPowerEvaluator()Evaluate a power polynomial by a corrected level-aware Horner chain.
For degree
and then applies
A constant polynomial deliberately consumes one level by multiplying the input by zero. A linear polynomial consumes one coefficient-multiplication level and requires no relinearization key. Degree
required_levels
method
def required_levels(polynomial: PolynomialApproximation) -> int: ...Return one level for constants or the declared power degree.
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return the exact corrected-Horner multiplication inventory.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, polynomial: PolynomialApproximation, *, relinearization_key: RelinearizationKey | None=None) -> Ciphertext: ...Evaluate a power polynomial with a corrected Horner recurrence.
The input must be a complete two-component coefficient-domain, standard-RNS Q ciphertext at the engine default scale. The method validates all context, key, scale, and available-depth requirements before allocating encrypted temporaries. The functional output is in the same arithmetic state at ciphertext.level + required_levels(polynomial) and default scale.
PatersonStockmeyerPowerEvaluator
class View source
PatersonStockmeyerPowerEvaluator(baby_step: int)Evaluate a power polynomial with one fixed baby-step size.
baby_step=k is part of the evaluator identity and is never selected at runtime. The evaluator writes
Balanced shared powers
The declared coefficient tuple, including zero entries, fixes the schedule. baby_step therefore controls a reproducible DAG rather than an unreliable degree-only estimate.
Attributes
| Name | Type | Default/value |
|---|---|---|
baby_step | int |
required_levels
method
def required_levels(polynomial: PolynomialApproximation) -> int: ...Return the exact critical-path level cost for this fixed k.
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return exact ciphertext, coefficient, and alignment counts.
evaluate
method
def evaluate(engine: CkksEngine, ciphertext: Ciphertext, polynomial: PolynomialApproximation, *, relinearization_key: RelinearizationKey | None=None) -> Ciphertext: ...Evaluate with the fixed baby/giant schedule and level caches.
PolynomialApproximation
class View source
PolynomialApproximation(basis: PolynomialBasis, coefficients: tuple[complex, ...], domain: tuple[float, float] = (-1.0, 1.0), name: str = 'polynomial', max_error: float | None = None)An immutable polynomial produced independently of its evaluation DAG.
The coefficient convention is ascending degree. For basis="power",
while basis="chebyshev" means
domain=(a, b) records the physical interval used to design the approximation. The evaluator input is nevertheless normalized (a, b) == (-1, 1); the caller owns the affine map.
Attributes
basis: Basis in whichcoefficientsare expressed.coefficients: Ascending coefficients: entryimultiplies either or .domain: Plaintext interval on which the approximation was designed.name: Human-readable diagnostic name.max_error: Optional sampled or certified approximation error.
Attributes
| Name | Type | Default/value |
|---|---|---|
basis | PolynomialBasis | |
coefficients | tuple[complex, ...] | |
domain | tuple[float, float] | (-1.0, 1.0) |
name | str | 'polynomial' |
max_error | float | None | None |
degree
property
degree: intReturn the algebraic degree including trailing zero entries.
evaluate_plaintext
method
def evaluate_plaintext(values: np.ndarray) -> np.ndarray: ...Evaluate
values may have any NumPy-broadcastable shape, which is preserved in the output. They are coordinates in the polynomial's basis domain. For a Chebyshev approximation created on a physical interval other than
Radix2FourierTransformCompiler
class View source
Radix2FourierTransformCompiler(stage_count: int, imaginary_unit_correction: bool = False)Synthesize CKKS basis transforms from radix-2 butterflies.
Let coeffs_to_slots map and slots_to_coeffs map in the engine's cyclotomic slot order. The compiler's convention is
Consequently a plaintext round trip uses forward scale=1 and inverse scale=1/S. The supplied scale multiplies the numerical map; it is not a CKKS metadata scale and does not change the diagonal plaintext encoding scale selected later by the evaluator.
stage_count controls only algebraic layer collapse. A smaller value consumes fewer CKKS levels but materializes more diagonals in each stage; a larger value retains sparse butterflies but spends more levels. The choice of direct, BSGS, distributed, or custom execution remains independent.
Attributes
| Name | Type | Default/value |
|---|---|---|
stage_count | int | |
imaginary_unit_correction | bool | False |
compile
method
def compile(*, slots: int, direction: TransformDirection, generator: int, scale: float=1.0) -> tuple[DiagonalLinearTransform, ...]: ...Compile
Compilation proceeds in four steps:
- validate the cyclotomic slot orbit and build root tables;
- construct one exact three-diagonal transform per radix-2 layer;
- compose adjacent layers according to
stage_count; - fold
scaleinto the first forward stage or final inverse stage so the normalization is applied once in the complete transform.
slots is [slot]. generator must enumerate direction chooses complex128 stages in online execution order; compilation performs no encryption.