DSP - BLOCK COVARIANCE
The covariance of TWO input streams over a block of N consecutive sample PAIRS: cov(a,b) = mean(ab) - mean(a)mean(b). This is the first two-input block of the family - IN_A and IN_B are sampled TOGETHER by ONE IN_DV, so the pairing is positional and the two streams cannot slip inside a block. N is a power of two chosen at RUN TIME on the EXP input pin (EXP = 10 means N = 1024), so it is computed as the exact integer cov_num = NSab - SaSb and then divided by N^2 with a shift: the answer is BIT EXACT, tolerance ZERO against an exact-rational golden. UNLIKE THE VARIANCE IT CAN BE NEGATIVE, and negative is the interesting case - hence the SIGNED default output format. The only serial arithmetic is the single product Sa*Sb, so the tail is short: L = 2 + (IN_SW + EXP + 1), the same shape as Block Variance, and it does not grow with Max Block Exponent. IN_DV is the only qualifier and there is deliberately no CE pin. Optional BUSY / INTEGRATING / SAMPLE_COUNT status outputs. Blocks of up to 2^20 sample pairs out of the box, 2^31 if you ask for it.
Introduction
The Block Covariance block chops two input streams into consecutive blocks of N sample pairs and, at the end of each block, publishes the covariance of that block:
$$ \mathrm{cov}(a,b) = \frac{1}{N}\sum_{i=0}^{N-1} a_i b_i - \left(\frac{1}{N}\sum_{i=0}^{N-1} a_i\right) \left(\frac{1}{N}\sum_{i=0}^{N-1} b_i\right) $$
The covariance is the joint variability of two channels. It is positive when they rise and fall together, negative when one rises as the other falls, and zero when there is no linear relationship between them.
TWO inputs, ONE qualifier
IN_A and IN_B are sampled on the same clock, by the same IN_DV. The
pairing is therefore positional: pair i of a block is whatever was on
the two pins on the i-th clock that IN_DV was high, and there is no way
for the two streams to slip relative to each other inside a block. There is
no second data-valid, no per-stream FIFO and no alignment logic to get wrong -
align the two channels upstream, once, and the block does the rest.
For the same reason both pins carry the SAME input Q format: one set of
IN properties (integer bits, fractional bits, sign) covers IN_A and
IN_B. That is what keeps the internal widths - and therefore the cost -
simple. If your two signals genuinely have different formats, requantise one
of them upstream with a Fixed P. block so that both arrive in the same
format.
N is a runtime input, not a property
You drive the exponent on the EXP pin and the block size is
$N = 2^{\mathrm{EXP}}$ pairs:
| EXP | N | EXP | N |
|---|---|---|---|
| 4 | 16 | 12 | 4096 |
| 6 | 64 | 16 | 65536 |
| 8 | 256 | 20 | 1048576 |
Because N is a power of two, both divisions are exact arithmetic shifts. There is no divider anywhere in this block.
What it is FOR
The covariance is the number you want when the magnitude of the joint variation matters, in the units of the two signals:
- cross-channel coupling / crosstalk measured in real units - “channel B moves by so many LSB per LSB of A” - rather than as a dimensionless score;
- the numerator of a linear fit: the least-squares slope of b against a is $\mathrm{cov}(a,b)/\mathrm{var}(a)$, so this block plus a Block Variance on the same stream gives you a two-channel regression;
- noise covariance between two detector channels, which is what tells you whether a common-mode subtraction will help;
- anti-correlation detection: two channels of a differential pair, or a pair of PMTs on opposite sides of a scintillator, should show a strongly negative covariance, and a covariance that drifts towards zero is a real fault signature.
Tie IN_B to IN_A and the covariance is the variance of that channel -
a free, exact self-check you can run on live hardware.
Covariance or correlation?
Use this block when you care about how much - the answer is in the units of $a \times b$ and it scales with the gain of either channel.
Use Block Correlation when you care about whether - the Pearson coefficient $r = \mathrm{cov}/(\sigma_a\sigma_b)$ is dimensionless, bounded to $[-1,+1]$ and gain independent, which is exactly what the covariance cannot give you. A covariance of 5000 means nothing on its own; an $r$ of 0.98 means the two channels are the same signal.
The price is that Block Correlation has to compute two standard deviations, a square root and a division, and it has the longest tail in the family. This block has a single serial product and a tail of a few tens of clocks.
Cost
Per clock: one multiplier ($a \cdot b$, unavoidable at one pair per clock), three accumulators ($S_a$, $S_b$, $S_{ab}$) and the block counter. In the tail: one reused shift-add stage for $S_a \cdot S_b$, stepped once per clock. There is no DSP in the tail and no divider anywhere.
When to use this instead of Block Statistics
The all-in-one Block Statistics block is not deprecated, but it does not compute a covariance: it is a single-stream block and has no cross-product accumulator, so there is no counterpart to this statistic over there. The usual rule still applies to everything else you may want from the same data:
- you want several statistics of the SAME single stream - mean and RMS and min/max of the same N samples - use Block Statistics. They share one accumulator and one serial tail, so the second and third statistic are nearly free.
- you want exactly one number - use a per-operator block. Then you synthesise only that number: the pin list, the logic and the tail are all that it needs, and nothing else reaches the synthesiser.
A Block Statistics block placed next to this one to get, say, the mean of channel A will duplicate the $S_a$ accumulator. That duplication is unavoidable here and it is one adder wide - it is not a reason to avoid either block.
Pin Description
IN_B, accumulated, and counted towards N, only on the clocks where
IN_DV is high. Tie it to IN_B and the block reports the variance
of that channel - a free exact self-check.
IN_A - one set of
IN properties covers both pins. It is sampled on the same clock as
IN_A, by the same IN_DV, so the pairing is positional and the two
streams cannot slip relative to each other inside a block. Align the two
channels, and match their Q formats, upstream.
IN_A and IN_B are both taken, and the pair counts towards N,
exactly on the clocks where this is high; the serial tail keeps running
regardless. Unconnected defaults to '1'. (There is deliberately no CE
pin - to stall the block, gate this.)
OUT_DV clock and on no other;
it holds the previous block’s result until then.
COVARIANCE is updated on this
clock and on no other. BUSY is still high here and falls on the next
clock. A block whose result was dropped for being too short produces no
pulse at all - that is the only symptom.
OUT_DV pulse, and it falls on the clock after. On a continuous
stream it simply stays high. Present on the symbol only when Enable BUSY
= YES.
BUSY high with INTEGRATING low is exactly the serial multiply.
On a continuous stream it dips for one clock per block boundary, which
makes it a free block marker. Present on the symbol only when Enable
INTEGRATING = YES.
How many sample PAIRS have been accumulated so far in the current block: 1 after the first, N after the N-th. It is NOT cleared at the end of a block
- it HOLDS the final count through the tail and past
OUT_DV, until the first pair of the next block takes it back to 1, it reads N atOUT_DVonly when the input STOPS for the whole tail. On a CONTINUOUS stream the next block has already started by then, soOUT_DVshows how far into it the input has got, not N - the clock that always reads N is the oneINTEGRATINGfalls on. OnlyRESETclears it to 0. Fixed 32 bits. Present on the symbol only when Enable SAMPLE_COUNT = YES.
Properties
Number of INTEGER bits of the input sample (the sign, when present, uses one of them).
Integer bits of the input samples (the sign, when present, uses one of them). 1..64. Default 16. It applies to BOTHIN_A and IN_B - the
two streams share one Q format.
Default: 16
Options: 1 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Number of FRACTIONAL bits of the input sample, i.e. the bits to the right of the binary point. Total width = integer + fractional bits, and must not exceed 64.
Fractional bits of the input samples, for both pins. 0..64. Total input width must be 2..64 bits. Default 0. Remember that the covariance carries two factors of $2^{-\mathrm{IN_{fract}}}$, so $\mathrm{COV_{fract}} = 2 \times \mathrm{IN_{fract}}$ is the format that keeps the natural scale.Default: 0
Options: 0 1 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Select whether the input sample is signed (two’s complement) or unsigned.
SIGNED (two’s complement) or UNSIGNED inputs, for both pins. Default SIGNED. An UNSIGNED input costs one extra bit internally, because a sample has to be promoted to signed before it can be accumulated - and that bit also adds one clock to the serial tail.Default: SIGNED
Options: UNSIGNED SIGNED
Largest block-size exponent the accumulators are sized for: the block can be up to 2^MaxBlockExponent samples long. The EXP input is clamped to this value at run time. Raising it widens the internal accumulators, and ON THE BLOCKS WHOSE SERIAL ENGINES ARE SIZED FROM THOSE ACCUMULATORS (Coefficient of Variation, SNR, Skewness, Kurtosis, Correlation, Autocorrelation, Linear Regression) it also LENGTHENS THE SERIAL TAIL – even when the runtime EXP is small. Keep it at the largest block you actually use. The default of 20 covers blocks of up to 1048576 samples.
Largest block-size exponent the accumulators are sized for: the block can be up to $2^{\text{MaxBlockExponent}}$ sample pairs long, and theEXP
input is clamped to this value at run time. Raising it widens the internal
$S_a$, $S_b$ and $S_{ab}$ registers by one bit per unit; it does NOT
lengthen the tail of this block, which depends only on the input width and
on the runtime exponent. It must be at least the minimum usable
exponent of the configuration (5 for a 16 bit signed input), or the
property page refuses the settings. 1..31, default 20, i.e. blocks of
up to 1048576 pairs out of the box.
Default: 20
Options: 1 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 30 31
Number of INTEGER bits of the COVARIANCE output (the sign, when present, uses one of them).
Integer bits of the COVARIANCE output. 1..64, default 32. Size it for $\max|a| \cdot \max|b|$: about twice the input integer bits is always enough, which is exactly what the default is for the default 16 bit input.Default: 32
Options: 1 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Number of FRACTIONAL bits of the COVARIANCE output, i.e. the bits to the right of the binary point. Total width = integer + fractional bits, and must not exceed 64.
Fractional bits of the COVARIANCE output. 0..64, total width 2..64 bits, default 0. Set it to $2 \times$ IN Fractional Bits to keep the natural scale of the product; ask for more if you want sub-LSB resolution on a small covariance - the accumulators carry those bits exactly and they are simply discarded by the final shift if you do not ask for them. It costs register width only, never a clock.Default: 0
Options: 0 1 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Select whether the COVARIANCE output is signed (two’s complement) or unsigned.
SIGNED or UNSIGNED COVARIANCE output. Default SIGNED, and you should leave it that way: unlike a variance a covariance is genuinely bipolar, and an UNSIGNED format silently clamps every anti-correlated block to 0 - the very case you are usually looking for. Choose UNSIGNED only when the two channels can never move in opposite directions and you want the extra bit.Default: SIGNED
Options: UNSIGNED SIGNED
YES: the BUSY (high from the first sample of a block until its result is out – it COVERS THE SERIAL TAIL, and its last high clock IS the OUT_DV pulse) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theBUSY pin exists. It is high from the first pair of a block
until its result is out, tail included, and its last high clock is the
OUT_DV pulse. NO: the pin and its register are removed before synthesis.
Default NO.
Default: NO
Options: NO YES
YES: the INTEGRATING (high only while the block is ACCUMULATING; it drops as soon as the N-th sample has been taken and the tail starts, so BUSY-and-not-INTEGRATING means ‘computing’) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theINTEGRATING pin exists. It is high only while the block is
accumulating, so BUSY high with INTEGRATING low means “the pairs are
all in, the serial multiply is running”. NO: the pin and its register are
removed. Default NO.
Default: NO
Options: NO YES
YES: the SAMPLE_COUNT (32 bit, how many samples have been accumulated so far in the current block: 1 after the first, N after the N-th. It is NOT cleared at the block end – it holds N until the NEXT block’s first accepted sample takes it back to 1. On a CONTINUOUS stream that happens DURING the serial tail, so at OUT_DV it reads how far into the next block the input has already got, NOT N. To capture the length of the block being presented, latch SAMPLE_COUNT on the clock INTEGRATING falls – that one always reads N) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theSAMPLE_COUNT pin exists - a fixed 32 bit count of the sample
pairs accumulated so far in the current block, holding the final count
through the tail and past OUT_DV. NO: the pin and its counter are
removed. Default NO.
Default: NO
Options: NO YES
ROUND: round to nearest when a result has to be requantised into a coarser output format. TRUNCATE: drop the bits (cheaper, adds a negative bias).
ROUND: round to nearest when the result has to be requantised into a coarser output format. TRUNCATE: drop the bits (cheaper, adds a negative bias). This is the only rounding in the block - everything before it is exact integer arithmetic. Default ROUND.Default: ROUND
Options: TRUNCATE ROUND
YES: clip to the largest representable value of each output format (symmetric for signed formats). NO: wrap around.
YES: clip to the largest representable value of the COVARIANCE format (symmetric bounds for signed formats). NO: wrap around. It matters when the output format is too narrow for the value, which - unlike a mean - can happen here if you sizeCOVARIANCE for less than
$\max|a| \cdot \max|b|$. Default YES.
Default: YES
Options: NO YES
The exact-integer form, and why the result is BIT EXACT
The obvious way to evaluate the definition is $(S_{ab}/N) - (S_a/N)(S_b/N)$, with three truncated quotients that then fight each other. This block does not do that. It uses the same trick Block Variance uses, and clears the denominators first:
$$ \mathrm{cov_num} = N,S_{ab} - S_a S_b, \qquad \mathrm{cov} = \frac{\mathrm{cov_num}}{N^2} $$
with
$$ S_a = \sum_i a_i, \qquad S_b = \sum_i b_i, \qquad S_{ab} = \sum_i a_i b_i $$
All three accumulators are exact integers. $N S_{ab}$ is a shift (N is a power of two). $S_a S_b$ is an exact integer product computed serially. The final division by $N^2$ is a shift again. So:
The only rounding anywhere in this block is the single final requantisation
into the Q format you chose for the COVARIANCE pin. There is no
accumulated rounding, no truncated intermediate and no approximation in the
datapath.
That is not an aspiration. The host regression (tb/block-ops/run_tb.ps1)
demands tolerance ZERO against a Python golden (tb/block-ops/ gen_golden.py) that evaluates the definition above in exact rational
arithmetic - not “within 1 LSB”, not “within a few counts”. Any deviation at
all fails the build.
The sign is carried as a bit, not through the datapath
The serial engine that forms $S_a S_b$ is an unsigned shift-add stage. The core hands it the two magnitudes, remembers whether exactly one of $S_a$ and $S_b$ was negative, and negates the product once, at the end. That is cheaper than a signed serial multiplier and it is exact.
THIS ONE CAN BE NEGATIVE
Block Variance can prove its own result non-negative (Cauchy-Schwarz) and therefore defaults to an UNSIGNED output. The covariance cannot, and must not. $\mathrm{cov_num}$ is a genuine signed quantity and there is no non-negativity clamp in this core - such a clamp would be a bug here, not a guard.
A negative covariance is not an error case, it is the interesting case: two anti-correlated channels. So:
The
COVARIANCEoutput format defaults to SIGNED, and you should leave it that way. An UNSIGNED format silently clamps every anti-correlated block to 0 - the block will look uncorrelated when it is in fact perfectly anti-correlated. Choose UNSIGNED only when you know from the physics that the two channels can never move in opposite directions, and you want the extra bit.
Units and sizing the COVARIANCE format
A covariance has the units of $a \times b$ - two factors of $2^{-\mathrm{IN_fract}}$, exactly like Block Mean Square, not one like Block Mean. So:
- to keep the natural scale, set $\mathrm{COV_{fract}} = 2 \times \mathrm{IN_{fract}}$;
- for the range, $|\mathrm{cov}| \le \max|a| \cdot \max|b|$, so about twice the input integer bits is always enough.
The default is exactly that: an IN of Q16.0 SIGNED ($|a| \le 2^{15}$)
gives $|\mathrm{cov}| \le 2^{30}$, which needs 31 bits plus a sign - and
COVARIANCE defaults to Q32.0 SIGNED. If you narrow the format below
what the data needs, the requantiser saturates (or wraps, if you turned
Saturation off).
Accumulation and IN_DV
IN_DV is the only qualifier. It says “this clock carries a sample
PAIR”: IN_A and IN_B are multiplied together, both are added to their own
accumulators, and the pair counts towards N, exactly on the clocks where
IN_DV is high. Clocks with IN_DV low are ignored completely - whatever
sits on the two input pins during them cannot corrupt the block - while the
tail keeps running, which is what you want: the tail has nothing to do with
the input stream.
Unconnected, IN_DV ties to '1' and EXP ties to 10 (N = 1024), so the
block free-runs with nothing wired except IN_A and IN_B.
There is deliberately no CE pin. On the all-in-one Block Statistics block an earlier revision had one, and it did not survive synthesis: with nothing but internal state gated by it, Vitis could reason the frozen path away and delete the port from the generated entity while SciCompiler’s wrapper still wired it, which failed a real Vivado build with [VRFC 10-718] formal port <ce> does not exist in entity. The whole per-operator family was built without one. To stall this block, gate its
IN_DV- a block that only accumulates onIN_DVhas no need to be frozen, and there is no reason to stall the tail.
When EXP changes
EXP is clamped to Max Block Exponent and then latched on the first
accepted sample pair of a block, and held for that whole block. A change
therefore takes effect on the NEXT block: a block in progress always
finishes against the N it was started with, and a block is never emitted
against a different N than the one it was accumulated with. Both the
$N S_{ab}$ shift and the final $N^2$ shift use that latched exponent, so the
arithmetic of a block is always self-consistent. You can sweep EXP from a
register interface while the streams run and every result stays correct.
Timing: the serial tail
Why it is serial
A block has a whole block period of slack after its N-th pair: the next
result is not due for another $2^{\mathrm{EXP}}$ clocks. So there is no reason
to build a parallel datapath for the post-accumulation arithmetic. II=1 is
only needed while ACCUMULATING - one pair per clock, one multiply, three
adds.
$S_a \cdot S_b$ is therefore ONE reused shift-add stage stepped once per clock, consuming one bit of $|S_b|$ per clock. The cost is CLOCKS, not multipliers: the only multiplier in the whole block is the $a \cdot b$ during accumulation, and there is no DSP in the tail.
The latency contract
OUT_DV pulses for one clock when the tail COMPLETES, L clocks after the
clock on which the N-th pair of the block was accepted - not when that pair
arrives. COVARIANCE is updated on that same clock and on no other. The state
walk is IDLE -(go)-> MUL x (IN_SW + EXP) -> VAR -> FIN -> IDLE with
OUT_DV registered, so
$$ L = 2 + (\mathrm{IN_SW} + \mathrm{EXP} + 1) $$
where $\mathrm{IN_SW}$ is the signed working width of a sample: the input width, +1 if the input is UNSIGNED (a sample has to be promoted to signed before it can be accumulated). $|S_b|$ is at most $\mathrm{IN_SW} - 1 + \mathrm{EXP}$ bits wide for a block of $2^{\mathrm{EXP}}$ pairs, so $\mathrm{IN_SW} + \mathrm{EXP}$ shift-add steps always clear it.
Two things are absent from that formula and both are worth noticing:
- the output format does not appear. Widening
COVARIANCEcosts registers, not clocks. MaxBlockExponentdoes not appear either. Raising it widens the accumulators but does not lengthen this tail - only the runtime exponent does. Block Correlation and Block Autocorrelation are not so kind.
Like Block Variance, this is one of the blocks whose latency depends on the runtime exponent. But a longer block also gives more slack: the block grows exponentially in EXP while the tail grows only linearly, so the $2^{\mathrm{EXP}} \ge L$ constraint gets easier as EXP grows, not harder.
Worked numbers
For the default configuration - IN Q16.0 SIGNED, so
$\mathrm{IN_SW} = 16$ - at EXP = 10:
- $L = 2 + (16 + 10 + 1) = \mathbf{29}$ clocks
- $2^{10} = 1024 \ge 29$, with a factor of 35 to spare
and the smallest usable exponent is the smallest EXP with $2^{\mathrm{EXP}} \ge 19 + \mathrm{EXP}$:
- EXP = 4: $16 \ge 23$? no
- EXP = 5: $32 \ge 24$? yes - so EXP $\ge$ 5, i.e. N $\ge$ 32
A few configurations, to show where the clocks go:
| Input | IN_SW | L | L at EXP = 10 | minimum EXP |
|---|---|---|---|---|
| 8 bit SIGNED | 8 | 11 + EXP | 21 | 4 (N = 16) |
| 16 bit SIGNED | 16 | 19 + EXP | 29 | 5 (N = 32) |
| 16 bit UNSIGNED | 17 | 20 + EXP | 30 | 5 (N = 32) |
| 32 bit SIGNED | 32 | 35 + EXP | 45 | 6 (N = 64) |
The 2^EXP >= L rule, and what happens when it is broken
The tail of one block must finish before the next block completes:
$$ 2^{\mathrm{EXP}} \ge L $$
If a block completes while the previous tail is still running, that block’s
result is DROPPED: there is no OUT_DV for it, the accumulators are
unaffected and the following blocks come out correctly, but a result is
silently skipped. There is no error pin and no overrun pin for it.
The property page refuses a configuration whose minimum exponent exceeds
Max Block Exponent, and CompileHDL prints both numbers - the worst
case tail length and the minimum usable EXP - in the compilation log, so the
static half of the problem is caught for you.
But EXP is a PIN. Nothing can stop a design driving it too low at RUN
TIME. On this block the margin is generous - the minimum is EXP = 5 for a
16 bit input - but if EXP is under software control, clamp it in software to
the minimum the compiler printed.
Knowing where the block is: BUSY, INTEGRATING and SAMPLE_COUNT
Three optional status outputs, all defaulting to NO. They answer different questions:
INTEGRATING |
BUSY |
|
|---|---|---|
| accumulating the block | 1 | 1 |
| serial tail computing | 0 | 1 |
| idle | 0 | 0 |
Every output of this block is a register, so each status bit is observed on the clock after the event that sets it:
INTEGRATINGrises on the clock after the FIRST pair of a block is accepted and falls on the clock after the N-th - it is high exactly while the block is ACCUMULATING.BUSYhigh withINTEGRATINGlow is precisely the serial multiply.BUSYcovers the accumulation and the tail. It rises withINTEGRATING, stays high across the tail, and its LAST HIGH CLOCK IS THEOUT_DVPULSE; it falls on the clock after.- On a continuous stream the next block starts before the previous tail
ends, so
BUSYnever drops andINTEGRATINGdips for exactly one clock per block boundary - which makes it a free block marker. SAMPLE_COUNTis a fixed 32 bits and reads 1 after the first accepted pair, N after the N-th. It is NOT cleared at the block end: it HOLDS N through the tail and pastOUT_DV, until the first pair of the next block takes it back to 1. it reads N atOUT_DVonly when the input STOPS for the whole tail. On a CONTINUOUS stream the next block has already started by then, soOUT_DVshows how far into it the input has got, not N - the clock that always reads N is the oneINTEGRATINGfalls on - which is the useful thing to latch alongside the result, and the cheapest way to confirm at run time that the block was long enough for the tail. OnlyRESETclears it to 0.
Q formats
IN_A, IN_B (one shared format) and COVARIANCE carry fixed point formats
(integer bits, fractional bits, sign), the same convention as the Fixed P.
family. The result is requantised into the COVARIANCE format with the
selected rounding (nearest / truncate) and overflow policy (saturate / wrap);
saturation is symmetric for signed formats, as everywhere else in the
toolchain.
Verification
The core is regression tested by a host-side csim harness
(tb/block-ops/run_tb.ps1) that runs one simulated clock at a time and
follows OUT_DV. The expected values come from tb/block-ops/gen_golden.py,
which evaluates $\mathrm{mean}(ab) - \mathrm{mean}(a)\mathrm{mean}(b)$ in
exact rational arithmetic and shares no algorithm with the core; the tolerance
is 0. Coverage pins the two-stream behaviour with second streams derived
from the first so the answer is known in advance:
| scenario | second stream | what it proves |
|---|---|---|
CV2_SAME_E5 |
$b = a$ | the covariance is the variance |
CV2_NEG_E5 |
$b = -a$ | the covariance is the negative of the variance - the one shape that proves the sign of the serial product is carried |
CV2_Q_E5 |
$b = a/2$ | a fractional COVARIANCE format (Q20.6) |
CV2_INDEP_E5 |
independent | a covariance near zero |
CV2_CONST_E5 |
constant | a covariance of exactly zero |
CV2_NOISY_E6 |
$a/2$ + noise on a sine | a realistic partial correlation |
CV2_EXPCHG |
$b = a$ | EXP changing every other block |
The tail length is checked against the formula at compile time: three
copies of it exist - the BCV_TAIL macro in the core, tail_len() in the
generator and TailClocks() in the plugin - and the testbench refuses to
build if they disagree. tb/block-ops/port_check.py preprocesses the real
core and checks the symbol’s pin list against the generated entity’s ports for
every enable combination. The status outputs are checked clock by clock
against the contract above. Unlike Block Mean or Block Variance there is no
all-in-one counterpart to cross-check against - Block Statistics has no
covariance - so the exact-rational golden is the only reference, which is why
its tolerance is zero.