fhelium.experimental.bootstrap.linear
Packed-slot linear-transform representations, compilers, and evaluators.
DiagonalBSGSEvaluator
class View source
DiagonalBSGSEvaluator(baby_step: int, hoist_baby_rotations: bool = True, aggregate_groups: bool = False, baby_steps_by_transform: Mapping[str, int] | None = None)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 depth/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 direct baby-step keys are available. Compact power-of-two inventories compose rotations through the private key-aware evaluation helper. baby_steps_by_transform may override the fallback step for named compiled transforms, keeping a stage-specific BSGS schedule inspectable without changing the transform representation. aggregate_groups applies all giant-group plaintext rows to the shared baby ciphertexts in one represented RNS operation; it changes execution grouping but not the BSGS partition or arithmetic.
Attributes
| Name | Type | Default/value |
|---|---|---|
baby_step | int | |
hoist_baby_rotations | bool | True |
aggregate_groups | bool | False |
baby_steps_by_transform | Mapping[str, int] | None | None |
required_depths
method
def required_depths(transform: Any) -> int: ...Return the single rescale consumed by one BSGS stage.
baby_step_for
method
def baby_step_for(transform: DiagonalLinearTransform) -> int: ...Return the caller-selected BSGS step for one compiled transform.
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(arithmetic: BootstrapArithmetic, 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 depth and actual scale
Here Delta_p is the selected diagonal plaintext scale and M_d is the dropped Q-group product.
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 shape [slot]. The returned CPU complex128 array has shape [slot]. This plaintext oracle does not encode, rescale, consume depths, 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 removed Q group has product
and Q prime_ids with the complete group 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_depths
method
def required_depths(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(arithmetic: BootstrapArithmetic, 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 depth, multiplied into its rotated ciphertext, and accumulated at pending scale. A single final rescale advances the output by one depth.
Raises
TypeError: Iftransformuses another stage representation.ValueError: If slot count or diagonal content is invalid.
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 depths but materializes more diagonals in each stage; a larger value retains sparse butterflies but spends more depths. 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 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.