Ciphertext state
Every ciphertext in your pipeline is represented by a HomValue that carries three pieces of metadata: pt_shape (the plaintext tensor shape), pt_scale (the precision of the encoded message), and levels on the modulus chain (which q-list rows and columns are still active).
HomValue in the debugger; or rely on the compile-time report later. See Inspecting the pipeline.Shape & slot packing
tensor_shape. pt_shape combines these two: tensor_shape plus which axis is the n axis. Whenever a reference page mentions "the n axis," this is what it means.Whichever axis wastes the least space against the number of available slots becomes the n axis automatically.
n_axis, set on the pipeline, forces a specific axis to be the n axis instead. Reach for this when the axis you actually want to matmul or broadcast over isn't the one the default picked.
Same rule as NumPy-style broadcasting, plus this one addition: mismatched n axes never combine, even if the shapes would otherwise line up.
A block holds exactly n / 2 slots. If your n axis is not a multiple of that, zeros are added to the end at encryption time, and trimmed off again when your result is decrypted. You never see them. A half-empty block costs the same as a full one, which is why the n axis defaults to the axis that leaves the least room empty.
tensor_shape, and whether it touches the n axis.Scale
pt_scale. Each multiplication multiplies the scales of its inputs, so a value at scale S becomes S² after one multiply. Left alone, scale outgrows the room a ciphertext has to represent it accurately.HomAdd, HomConstAdd: the output keeps the scale of its inputs.
HomMul, HomSquare output the product of their input scales; HomConstMul multiplies by the constant's own scale.
HomMatMul multiplies and sums, so the multiplication cost applies, and it's the fastest-growing operator in practice.
Reshapes, unfolds, repeats: they rearrange values without arithmetic, so scale passes through unchanged.
HomModSwitch, and it's the subject of the next page, Level budget & the modulus chain. The key takeaway is: multiplication increases ciphertext scale, and that growth must eventually be managed.In the HomOps Reference, every operator's Scale effect field tells you which of these behaviors applies.
Levels
full_q_list_precision. That chain is two-dimensional: rows and columns. As operators mod-switch, primes drop off the chain. What remains are the ciphertext's active rows and active columns.The indices you see are the rows and columns still available on that ciphertext, not necessarily a contiguous prefix of the full chain.
If a ciphertext has R active rows and C active columns, it has R × C remaining active levels.
HomValue while you debug.Why it has to stay in sync
Visualizing it yourself
shape, scale, and remaining q-list on each node. After you build, add --print_graph to lattica-build.# build the MNIST example and print the compiled graph
lattica-build --pipeline-module lattica_build.examples.example_mnist_fc --out /tmp/quickstart_mnist.zip --print_graphq=[…] is the remaining modulus chain.└─ [0.1.0] Mul keep_axis=False with_modswitch=False ├─ x shape=(@100, 1, 50) scale=1099511627776 q=[61] [45] ├─ x shape=(@100, 1, 50) scale=1099511627776 q=[61] [45] └─ → v0 shape=(@100, 1, 50) scale=1208925819614629174706176 q=[61] [45]
The tensor layout at that node. @ marks the n axis.
pt_scale at that point in the graph. Watch it grow after multiplies and come back down after mod-switch.
Remaining modulus-chain state: the q-list still active on that value. That is the ciphertext's remaining levels.
HomValue for the same three fields: pt_shape, pt_scale, and the active rows and active columns on the q-list. Remaining levels = (number of active rows) × (number of active columns). The indices tell you which levels remain, not only how many.HomValue shows active rows 5 and 7, and active columns 2, 3, and 5. That is 2 × 3 = 6 remaining active levels, and you know exactly which row/column indices are still available.The printed graph is the visual view of shape, scale, and levels on every node; the debugger inspects a single HomValue. Both stay on your machine. The compiler report runs at deploy. See Inspecting the pipeline for the full loop.
Next
with_modswitch) lives.