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 full-slot callable configured for one Engine. 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 depth, scale, and 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, and rescales by the complete Q group product while retaining actual scale. The final result is coefficient-domain standard RNS over Q at ciphertext.depth + required_depths(polynomial), with the corresponding prime_ids, unchanged batch shape, two components, and the coefficient-product target scale.
Attributes
| Name | Type | Default/value |
|---|---|---|
skip_near_zero | float | 0.0 |
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return multiplying operations executed by evaluate.
Coefficient multiplication counts only active nonconstant terms. Alignment multiplication counts every multiply-by-one depth advance, including advances used inside the shared power tree and before term addition. Additions and plaintext encoding are not multiplications.
required_depths
method
def required_depths(polynomial: PolynomialApproximation) -> int: ...Count the deepest balanced-product path and coefficient product.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, 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 depth by multiplying the input by zero; this keeps the execution behavior equal to the declared one-depth cost. Inputs are not mutated and output storage does not alias an input.
BootstrapArithmetic
class View source
BootstrapArithmetic(engine: Engine, constant_cache: MutableMapping[object, object] | None = None, retain_ntt: bool = True)Execute Bootstrap CKKS arithmetic with depth-dependent scale targets.
target_scales gives an inspectable multiplication schedule derived from Q group products. Scalar and diagonal products select their plaintext scales to reach that schedule; ciphertext products retain actual scales. constant_cache retains prepared scalar and monomial plaintexts for reuse across evaluation calls. retain_ntt=False selects coefficient-domain results for callers that compose Bootstrap without NTT-retaining Eager operations. FullSlotBootstrap creates this owner internally; custom compositions pass it to linear, polynomial, and periodic-reduction evaluators.
Attributes
| Name | Type | Default/value |
|---|---|---|
engine | Engine | |
constant_cache | MutableMapping[object, object] | None | None |
retain_ntt | bool | True |
target_scales
property
target_scales: tuple[float, ...]Return the selected per-depth arithmetic scales.
The default selection is computed backward from the terminal default scale using for_input follows an actual input scale forward over the requested depths. scaled_targets instead describes coefficient accumulators multiplied by unscaled basis nodes.
for_input
method
def for_input(ciphertext: Ciphertext, required_depths: int) -> BootstrapArithmetic: ...Follow
Polynomial basis nodes use this recurrence through their required depths; coefficient products may select a different output scale.
scaled_targets
method
def scaled_targets(factor: float) -> BootstrapArithmetic: ...Use a common scale factor for polynomial coefficient accumulators.
plaintext_scale
method
def plaintext_scale(input_scale: float, depth: int) -> float: ...Choose
multiply_by_monomial
method
def multiply_by_monomial(ciphertext: Ciphertext, exponent: int) -> Ciphertext: ...Return
align_depths
method
def align_depths(lhs: Ciphertext, rhs: Ciphertext) -> tuple[Ciphertext, Ciphertext]: ...Advance the shallower value to the deeper value's Q depth.
advance_depth
method
def advance_depth(ciphertext: Ciphertext) -> Ciphertext: ...Advance one Q group and return at the next target scale.
multiply_relinearize_rescale
method
def multiply_relinearize_rescale(lhs: Ciphertext, rhs: Ciphertext, *, relinearization_key: RelinearizationKey) -> Ciphertext: ...Return relinearized
multiply_scalar
method
def multiply_scalar(ciphertext: Ciphertext, scalar: complex) -> Ciphertext: ...Multiply by one encoded scalar, rescale, and return target scale.
weighted_scalar_sum
method
def weighted_scalar_sum(terms: Sequence[tuple[Ciphertext, complex]]) -> Ciphertext: ...Return
add_scalar
method
def add_scalar(ciphertext: Ciphertext, scalar: complex) -> Ciphertext: ...Add a scalar without changing depth, scale, domain, or RNS basis.
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 depth, active prime_ids, and depth-dependent scale schedule 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 multiplying operations executed by evaluate.
Alignment includes operand advancement within odd recurrences, the shared depth chain for rescale_operations counts one coefficient rescale per occupied basis depth rather than one per coefficient product.
required_depths
method
def required_depths(polynomial: PolynomialApproximation) -> int: ...Count the deepest required recurrence plus coefficient product.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, 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 depth so its execution agrees with required_depths. The method is functional and returns a two-component coefficient-domain standard-RNS Q value at the arithmetic owner's target 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, retain_ntt: bool = False, maximum_plaintext_error: float = 0.001)Reduce one real branch with a cosine seed and double-angle chain.
input_bound is the positive integer 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 Q ciphertext in either coefficient/standard or NTT/Montgomery representation, with payload axes [component, *batch, limb, coefficient], active prime_ids, and actual scale matching the arithmetic owner's target at the input depth. Evaluation is functional. With retain_ntt=True, a coefficient input is transformed once and the polynomial and double-angle chain retain NTT/Montgomery form until one final inverse transform. Otherwise each multiplication returns to the input representation. Rescale removes one Q group and divides actual scale by its product. The output uses the input representation, batch and component axes, Q basis, and context; its depth advances by required_depths, its limb axis contains the corresponding suffix of prime_ids, and its actual scale follows the arithmetic owner's depth schedule.
More iterations reduce the seed frequency but each iteration costs one ciphertext multiplication, relinearization, and rescale depth. 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 |
retain_ntt | 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_depths
property
required_depths: intCount explicit division, polynomial depth, and recurrence depth.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, ciphertext: Ciphertext, *, relinearization_key: RelinearizationKey | None, conjugation_key: ConjugationKey | None=None) -> Ciphertext: ...Evaluate
With non-fused normalization the input represents raw ciphertext.depth + required_depths. 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, 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.
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, depth transition, and depth-dependent scale schedule 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.depth + required_depths, with the corresponding active prime_ids and the arithmetic owner's target 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_depths
property
required_depths: intCount normalization, polynomial DAG, squarings, and sine scaling.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, 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: Engine, *, 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_depth: int=0, retain_diagonals: bool=False, retain_constants: bool=False, batch_modular_branches: bool=False)Full-slot refresh callable with prepared transforms and replaceable components.
Construction configures transform compilers/evaluators and modular reduction for one engine. Calling the object executes the visible full-slot algorithm with one validated evaluator-only key inventory.
retain_diagonals keeps operation-ready transform plaintexts between calls. retain_constants similarly keeps the scalar, entry, and monomial constants used by the circuit between calls. If batch_modular_branches is true, the real and imaginary periodic reductions share one dense branch-batch execution before being unpacked.
input_depth selects the penultimate Q group. The entry rescale reaches the ordinary terminal basis, which this composition uses for centered ModRaise. It reserves no private CKKS depth.
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, and key switching perturb this idealized map. The caller must establish the reducer's raw-coordinate precondition
required_rotations
property
required_rotations: tuple[int, ...]Return normalized signed
key_steps
method
def key_steps(strategy: str='direct') -> tuple[int, ...]: ...Return the rotation-key inventory for one composition strategy.
direct 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 notdirectorpower_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.
cached_constant_bytes
property
cached_constant_bytes: intReturn tensor bytes retained for scalar and structural constants.
clear_cache
method
def clear_cache() -> None: ...Release prepared constants, encoded diagonals, and arithmetic tables.
HornerPowerEvaluator
class View source
HornerPowerEvaluator()Evaluate a power polynomial by a corrected depth-aware Horner chain.
For degree
and then applies
A constant polynomial deliberately consumes one depth by multiplying the input by zero. A linear polynomial consumes one coefficient-multiplication depth and requires no relinearization key. Degree
required_depths
method
def required_depths(polynomial: PolynomialApproximation) -> int: ...Return one depth for constants or the declared power degree.
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return the corrected-Horner multiplication inventory.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, 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 its recorded actual 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.depth + required_depths(polynomial) and the arithmetic owner's target 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
Basis powers follow the input's actual scale recurrence. Coefficient accumulators use a common multiple of those scales chosen so the final product reaches the requested output scale. This keeps additions coherent without changing the polynomial's coefficients or its input coordinate.
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_depths
method
def required_depths(polynomial: PolynomialApproximation) -> int: ...Return the critical-path depth cost for this fixed k.
operation_inventory
method
def operation_inventory(polynomial: PolynomialApproximation) -> dict[str, int]: ...Return ciphertext, coefficient, and alignment counts.
evaluate
method
def evaluate(arithmetic: BootstrapArithmetic, ciphertext: Ciphertext, polynomial: PolynomialApproximation, *, relinearization_key: RelinearizationKey | None=None) -> Ciphertext: ...Evaluate with the fixed baby/giant schedule and depth 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 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.