Decision Tree — Machine Learning/Classical Models
Machine_Learning/Classical_Models/Decision_Tree · 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.
Decision Tree
Machine Learning / Classical Models
A fitted decision tree, evaluated at inference. The tree arrives as
five flat columns – exactly the arrays scikit-learn's tree_
exposes – and the block walks it from the root: at each internal node it
compares one feature against that node's threshold, going left when
u[Feature] ≤ Threshold and right otherwise, until it reaches a leaf and
outputs that leaf's value.
It is the workhorse of interpretable machine learning, and unlike almost everything else in this family it involves no transcendental and no arithmetic at all – only comparisons – so it is exact on every target, including hardware.
Ports
- u – the feature column, [d,1]. Every entry of Feature Index must name a row of it; d itself comes from whatever is connected, and is checked against the largest index used.
- Output – y, a [1,1] scalar carrying the value of the leaf reached. For a regression tree that is the predicted value; for a classifier, the predicted class label as you stored it in Leaf Value.
Parameters
All five are columns of the same height N, one row per node, with the
root at row 0. This is the layout scikit-learn already gives you:
tree_.feature, tree_.threshold,
tree_.children_left, tree_.children_right and
tree_.value.
- Feature Index – which entry of u the node tests, zero-based. Ignored at leaves, where scikit-learn stores −2.
- Threshold – the value it is tested against. Ignored at leaves.
- Left Child – the row taken when u[Feature] ≤
Threshold. A negative entry marks the node as a leaf, which is
scikit-learn's own convention (
children_leftis −1 there). - Right Child – the row taken otherwise. Ignored at leaves.
- Leaf Value – the output produced when the walk stops at this node. Ignored at internal nodes.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Children must have a higher row number than their parent. Every fitted tree exporter emits them that way, and requiring it means a malformed table is reported at the start of the run rather than sending the code generator into an endless walk.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. Every target is exact rather than merely within tolerance, and the three HDL targets are genuinely synthesizable: the block only compares and assigns.
The tree is emitted as a nested if cascade rather than as a loop over the five arrays. That is what keeps it synthesizable – a literal walk would need a data-dependent index into five arrays on every iteration, which in hardware is a multiplexer over every node, once per level. As a cascade each node appears exactly once and no index arithmetic exists at all, so the emitted code is proportional to the number of nodes rather than to nodes × depth.
Simulink bridge
None. The reference would be the Statistics and Machine Learning
Toolbox and its fitctree / predict blocks, which is
not installed on this machine – so a bridge could not be run against a
parity testbench even if one were written. Those blocks also take a fitted
model object from a workspace variable, which no parameter mapping can
carry: that is the same obstacle that makes this whole family
Support::None, and it is the reason the tree is pasted here as
plain columns instead. The bridge reports the 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.
- Piecewise constant. The output changes only when a sample crosses a split, so it is a staircase, not a curve – and its derivative is zero almost everywhere. That is worth remembering before putting one inside a feedback loop.
- No state space, deliberately, for exactly that reason: no A/B/C/D is true of a piecewise-constant map. Model reduction reports the block as unmergeable, which is the honest answer.
- The comparison is ≤ going left, matching scikit-learn. A sample landing exactly on a threshold therefore goes left, and in fixed point that is not a rare case – an HDL port delivers Q16.16, so a threshold that is exactly representable is landed on rather than approached.
- Random Forest, Gradient Boosted Trees and Isolation Forest Score are this same walk with a different accumulator over a stacked table.
Code facts#
| Fact | Value |
|---|---|
| registered type | Machine_Learning/Classical_Models/Decision_Tree |
| family | Machine_Learning/Classical_Models |
| solver environment class | ICoreBlock_0_Machine_Learning_1_Classical_Models_2_Decision_Tree |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Classical_Models/Decision_Tree/ICoreBlock_0_Machine_Learning_1_Classical_Models_2_Decision_Tree.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/Machine_Learning/Classical_Models/Decision_Tree/ICoreBlock_0_Machine_Learning_1_Classical_Models_2_Decision_Tree.h |
| default size on canvas | 130 × 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 |
|---|---|---|
Feature Index | [0; 1; -2; -2; -2] | — |
Threshold | [0.35; -0.2; 0; 0; 0] | — |
Left Child | [1; 2; -1; -1; -1] | — |
Right Child | [4; 3; -1; -1; -1] | — |
Leaf Value | [0; 0; -1.5; 0.4; 1.75] | — |
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 verifiable Simulink equivalent. The reference would be the Statistics and Machine Learning Toolbox, which is not installed on this machine, so a bridge could not be run against a parity testbench even if one were written. Its predict blocks also take a fitted model OBJECT from a workspace variable, which no parameter mapping can carry -- which is exactly why this block takes the tree as plain columns instead. Nothing in DSP System or Signal Processing, both of which are installed, is a decision tree
Catalog contract: src/ICoreSDK/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The checker has a blind spot here — it could not resolve something (a grouped port bullet, a computed config name), which is reported and never counted as a pass. A reader has to settle it:
B0every stimulus in the sample errored — cross-checks skipped
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).
Decision Tree — a fitted tree as five flat columns, evaluated at inference node = 0 while node is internal: node = (u[Feature[node]] <= Threshold[node]) ? Left[node] : Right[node] y = LeafValue[node]
The five columns are exactly what scikit-learn's
tree_exposes, including its leaf marker: node i is a LEAF when Left[i] < 0 (sklearn's TREE_LEAF is -1).⚠ The generated code is a NESTED IF CASCADE, not a loop over the arrays, and that is the decision that makes the HDL targets work at all. A literal walk needs a data-dependent index into five arrays every iteration -- in hardware, a mux over N nodes per level, plus fx_to_int to turn a Q16.16 child index into an address. Emitted as nested ifs, each node appears exactly ONCE, no index arithmetic exists anywhere, and the emitted size is O(N) rather than O(N x depth). Every tree-to-code exporter does this, for the same reason.
⚠ Children must have a HIGHER index than their parent. Every fitted-tree exporter emits them that way, and requiring it makes a cycle impossible by construction -- which matters here far more than it would elsewhere, because a cycle in the table would make the emitter recurse until the stack ran out rather than produce a diagnosable error. It is checked, with that reason in the message.
⚠ Take this block before Random_Forest, Gradient_Boosted_Trees and Isolation_Forest_Score: all three are this walker with a different accumulator over a stacked node table.
Sample results#
No stimulus produced a sampled output in this rig — Feature index out of range at: ICore Blocks/Home/Decision Tree. That is a fact about the single-block rig, not a verdict on the block: an offline batch fit, a block whose output only appears at onSolverFinish, or one that needs a driven environment cannot be exercised alone.
Category unsampled · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60
Sample data: docs/generated/samples/Machine_Learning__Classical_Models__Decision_Tree.json