Normalizer — Machine Learning/Preprocessing
Machine_Learning/Preprocessing/Normalizer · 1 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
Normalizer
Machine Learning / Preprocessing
Scales each sample to unit norm: y = u ÷ ‖u‖. It is
scikit-learn's Normalizer at inference, the preprocessing step a
model wants when only the direction of a feature vector carries
information and its magnitude does not – a spectrum, a histogram, a bag of
counts, or any signal whose overall gain drifts with the sensor.
Unlike the other scalers in this family it divides by a statistic of the current sample rather than by a fitted constant, so it needs no training output at all: there is nothing to paste in but the choice of norm.
Ports
- u – the feature column, [m,1]. Any m ≥ 1; the block reads the width from the signal rather than from a parameter, because the norm is taken over whatever arrives.
- Output – y, the same [m,1] shape. Its norm is 1 under the selected measure, except for the floored case below.
Parameters
- Norm – which measure is driven to 1.
- L2 – y = u ÷ √(Σ uᵢ²), the
Euclidean norm. The default, and what
Normalizer()uses unless told otherwise. - L1 – y = u ÷ Σ |uᵢ|. On a non-negative vector this turns counts into proportions that sum to 1.
- Max – y = u ÷ max |uᵢ|, which puts the largest entry at ±1 and leaves the rest inside it.
- L2 – y = u ÷ √(Σ uᵢ²), the
Euclidean norm. The default, and what
- Epsilon – a floor on the divisor: the block divides by max(‖u‖, Epsilon) rather than by the norm itself, so a zero or near-zero vector gives a finite answer instead of an infinity. It is one expression with no branch in it, which is what keeps all ten targets identical. On a zero vector this agrees exactly with scikit-learn (both give y = 0); it differs only for a vector whose norm is tiny but nonzero, which scikit-learn amplifies to unit length and this block does not. Raise it if your input has a noise floor you do not want stretched.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. The selected norm and Epsilon are baked into the generated code at export time; there is no tunable parameter, because changing the norm changes which arithmetic is emitted rather than a value it uses.
The three HDL targets are simulation-only, and this block cannot
be made synthesizable the way Standard Scaler is: that block divides by a
fitted constant, which can be inverted once at export time so the datapath only
multiplies, whereas this one divides by a value that is not known until the
sample arrives. The bodies read the port with to_real, compute in
real, and quantize back with to_fx – the same
pattern Recursive IIR and Dense Layer use. They simulate correctly
and are not offered for synthesis.
Simulink bridge
None – but unlike the rest of this family, not because nothing
comparable exists. DSP System Toolbox is installed here and ships
Normalization (dspmathops/Normalization). It was checked
rather than assumed, and it computes a different function:
- Its NormType offers a 2-norm and a squared 2-norm only. That covers this block's L2 and neither L1 nor Max, so a model configured for one of those would export as a 2-norm without saying so – exactly what a bridge exists to prevent.
- It divides by (norm + Bias) where this block divides by max(norm, Epsilon). Those agree only where neither term binds, so Epsilon cannot be mapped onto Bias even for the L2 case.
- It normalizes down each column (or row, or a named dimension) of a frame, whereas this block treats the input as one sample.
If your pipeline only ever uses the L2 norm, you can substitute that block
directly: set its Bias to 0 and keep the input a single column. The
bridge reports this block rather than dropping it silently, and it has no
parity testbench, which is the documented consequence of
Support::None rather than a gap. Code export verification still
covers it across all ten languages.
Notes
- Algebraic and stateless: the output depends only on the current sample, so the block cannot break an algebraic loop.
- Not elementwise. The norm reduces over the whole vector before any element is written, so entry i of the output depends on every entry of the input – unlike Standard Scaler, where the features are independent.
- No state space, deliberately. y = u ÷ ‖u‖ is homogeneous of degree zero – doubling the input leaves the output unchanged – so it is nonlinear and no A/B/C/D is true of it. Model reduction reports the block as unmergeable, which is the honest answer.
- The block is a column map: feed one sample at a time. A matrix input is reported rather than treated as a batch of samples.
- Scale-invariant, which is worth knowing when reading a rig: feeding this block a signal that is one waveform times a constant vector produces a constant output that only flips with the sign of the waveform. A stimulus with independent offsets is what actually exercises it.
Code facts#
| Fact | Value |
|---|---|
| registered type | Machine_Learning/Preprocessing/Normalizer |
| family | Machine_Learning/Preprocessing |
| solver environment class | ICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Preprocessing/Normalizer/ICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Preprocessing/Normalizer/ICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer.h |
| default size on canvas | 120 × 80 px |
| ports at insert | 1 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | u |
| 2 | out | ICoreDouble | y |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Norm | L2%~%L1%~%Max~~L2 | — |
Epsilon | 1e-12 | — |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
Caveat (shown to the user): no faithful Simulink equivalent, though a related block does exist. DSP System Toolbox is installed here and ships "Normalization" (dspmathops/Normalization), which was probed rather than assumed: it offers a 2-norm and a squared 2-norm only, and it divides by (norm + Bias) where this block divides by max(norm, Epsilon). So it covers one of this block's three norms and uses a different rule at the degenerate point. Mapping onto it would let a model configured for the L1 or Max norm export as a 2-norm silently, which is the failure a bridge exists to prevent. For an L2 pipeline you can substitute that block directly, setting its Bias to 0 and remembering that its normalization runs down each column
Catalog contract: src/ICoreSDK/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The lists agree. check_block_descriptions.py finds no disagreement between the description's Ports, Parameters, Code export and Simulink bridge lists and the code's.
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
Normalizer — sklearn's Normalizer, evaluated at inference: each SAMPLE scaled to unit norm L2: y = u / sqrt(sum(u_i^2)) L1: y = u / sum(|u_i|) Max: y = u / max(|u_i|)
⚠ Read the header before copying Standard_Scaler onto this block. Every other scaler in this family divides by a FITTED CONSTANT, so its divisor is inverted once at config load and the datapath multiplies. Here the divisor is a statistic of the CURRENT SAMPLE -- a signal, not a constant -- which has two consequences that run through the whole file:
- There is no reciprocal to precompute, so the three HDL targets are SIMULATION-ONLY
realrather than Standard_Scaler's synthesizable Q16.16. A per-sample divide, and for L2 a square root, is exactly what forces the escape hatch.
- The block is NOT elementwise. Every backend computes the denominator over the whole
vector FIRST, then the m quotients -- which is why eight of the ten bodies below carry a scratch variable that Standard_Scaler needed none of.
⚠ The DENOMINATOR IS FLOORED, not special-cased: d = max(||u||, Epsilon), one expression with no branch in it. That is deliberate. A branch ("if the norm is zero, pass the vector through") is a rule that has to be re-implemented identically in eleven places -- the block and ten generators -- and the family's own history says that is where they drift. A floor is a single expression every target already has a primitive for, so there is nothing to get inconsistent. It also agrees with scikit-learn on the case that actually occurs: sklearn replaces a zero norm by 1 and returns 0/1 = 0, this returns 0/Epsilon = 0. The two differ only for a vector whose norm is tiny but NONZERO, where sklearn amplifies it to unit length and this one does not -- the safer answer on a fixed-point target, and the description says so plainly rather than leaving a user to find it.
Sample results#
| t | in ICoreDouble-Out-0 | out ICoreDouble-Out-0 |
|---|---|---|
| 0 | -2 | -1 |
| 0.4 | 0.5 | 1 |
| 0.8 | -2 | -1 |
| 1.2 | 0.5 | 1 |
| 1.6 | -2 | -1 |
| 2 | 0.5 | 1 |
| 2.4 | -2 | -1 |
| 2.8 | 0.5 | 1 |
| 3.2 | -2 | -1 |
| 3.6 | 0.5 | 1 |
| 4 | -2 | -1 |
| 4.4 | 0.5 | 1 |
| 4.8 | -2 | -1 |
| 5.2 | 0.5 | 1 |
Every 4th of 60 samples, from the table stimulus.
The same rig also ran:
| Stimulus | What it is | Output range |
|---|---|---|
impulse | Impulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair) | 0 … 1 |
ramp | Ramp: slope 1 from t = 0 | 0 … 1 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -1 … 1 |
step | Step: 0 -> 1 at t = 1 s | 0 … 1 |
Plotted: table — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample
Category static · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60 · data docs/generated/samples/Machine_Learning__Preprocessing__Normalizer.json · the SVG is generated from those numbers by tools/docs/plot_svg.py, so it is a run and not a drawing (R-D10).