Generated reference › Normalizer — Machine Learning/Preprocessing
kind: generated#block#machine-learning-preprocessing

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.
  • Outputy, 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.
  • 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#

FactValue
registered typeMachine_Learning/Preprocessing/Normalizer
familyMachine_Learning/Preprocessing
solver environment classICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer
sourcesrc/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Preprocessing/Normalizer/ICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer.cpp
headersrc/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Preprocessing/Normalizer/ICoreBlock_0_Machine_Learning_1_Preprocessing_2_Normalizer.h
default size on canvas120 × 80 px
ports at insert1 in, 1 out
code generators implementedPython, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text

Ports#

#DirectionSignal typeDescription label
1inICoreDoubleu
2outICoreDoubley

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 variableDefaultSimulink parameter
NormL2%~%L1%~%Max~~L2
Epsilon1e-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.

supportSupport::None
Simulink path
port-count rulePortsParam::None
SampleTime parameteryes

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:

  1. There is no reciprocal to precompute, so the three HDL targets are SIMULATION-ONLY

real rather than Standard_Scaler's synthesizable Q16.16. A per-sample divide, and for L2 a square root, is exactly what forces the escape hatch.

  1. 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#

Normalizer — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sampleNormalizer — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample-1-0.500.51-2-10123inputoutput
tin ICoreDouble-Out-0out ICoreDouble-Out-0
0-2-1
0.40.51
0.8-2-1
1.20.51
1.6-2-1
20.51
2.4-2-1
2.80.51
3.2-2-1
3.60.51
4-2-1
4.40.51
4.8-2-1
5.20.51

Every 4th of 60 samples, from the table stimulus.

The same rig also ran:

StimulusWhat it isOutput range
impulseImpulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair)0 … 1
rampRamp: slope 1 from t = 00 … 1
sineSine Wave: amplitude 1, 2 rad/s, no phase, no bias-1 … 1
stepStep: 0 -> 1 at t = 1 s0 … 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).