lookup2D
2-D interpolated lookup table.
📝Syntax
Block type: lookup2D
📥Input Arguments
Parameter Description
input ports 2 input port(s) declared.
📤Output Arguments
Parameter Description
output ports 1 output port(s) declared.
📄Description

2-D interpolated lookup table.

Module nflow_blocks
Library Lookup Tables
Type lookup2D
Label 2-D Lookup Table

Description

Two inputs (row and column coordinates) index a static column-major matrix Table; bilinear / Flat / Nearest interpolation with Clip or Linear extrapolation.

This page describes the native runtime behavior observed in the module C++ sources. Declared phases indicate when the simulation engine calls the block.

Code generation: supported for C and Rust.

Implementation Sources

Manifestmodules/nflow_blocks/libraries/lookup/library.json
{
  "id": "builtin.lookup",
  "title": "Lookup Tables",
  "version": "0.1.0",
  "format": "nflow-2",
  "builtin": true,
  "blocks": [
    {
      "type": "lookup1D",
      "label": "1-D Lookup Table",
      "icon": "lookup1D.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "BreakpointsForDimension1": [
          0,
          1,
          2,
          3,
          4
        ],
        "Table": [
          0,
          1,
          4,
          9,
          16
        ],
        "InterpMethod": "Linear point-slope",
        "ExtrapMethod": "Linear"
      },
      "render": {
        "type": "image",
        "src": "exports/lookup1D.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "lookup2D",
      "label": "2-D Lookup Table",
      "icon": "lookup2D.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 30,
          "side": "left"
        },
        {
          "x": 0,
          "y": 50,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "BreakpointsForDimension1": [
          1,
          2,
          3
        ],
        "BreakpointsForDimension2": [
          1,
          2,
          3
        ],
        "Table": [
          4,
          5,
          6,
          5,
          7,
          8,
          6,
          8,
          10
        ],
        "InterpMethod": "Linear point-slope",
        "ExtrapMethod": "Linear"
      },
      "render": {
        "type": "image",
        "src": "exports/lookup2D.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "lookupND",
      "label": "n-D Lookup Table",
      "icon": "lookupND.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 30,
          "side": "left"
        },
        {
          "x": 0,
          "y": 50,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "NumberOfTableDimensions": 2,
        "BreakpointsForDimension1": [
          1,
          2,
          3
        ],
        "BreakpointsForDimension2": [
          1,
          2,
          3
        ],
        "Table": [
          4,
          5,
          6,
          5,
          7,
          8,
          6,
          8,
          10
        ],
        "InterpMethod": "Linear point-slope",
        "ExtrapMethod": "Linear"
      },
      "render": {
        "type": "image",
        "src": "exports/lookupND.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "directLookup",
      "label": "Direct Lookup Table (n-D)",
      "icon": "directLookup.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 30,
          "side": "left"
        },
        {
          "x": 0,
          "y": 50,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "NumberOfTableDimensions": 2,
        "TableDimensions": [
          2,
          3
        ],
        "Table": [
          0,
          1,
          10,
          11,
          20,
          21
        ]
      },
      "render": {
        "type": "image",
        "src": "exports/directLookup.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "prelookup",
      "label": "Prelookup",
      "icon": "prelookup.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 30,
          "side": "right"
        },
        {
          "x": 90,
          "y": 50,
          "side": "right"
        }
      ],
      "defaultParams": {
        "BreakpointsForDimension1": [
          0,
          1,
          2,
          3,
          4
        ]
      },
      "render": {
        "type": "image",
        "src": "exports/prelookup.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "interpolationPrelookup",
      "label": "Interpolation Using Prelookup",
      "icon": "interpolationPrelookup.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 30,
          "side": "left"
        },
        {
          "x": 0,
          "y": 50,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "Table": [
          0,
          1,
          4,
          9,
          16
        ]
      },
      "render": {
        "type": "image",
        "src": "exports/interpolationPrelookup.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    },
    {
      "type": "lookupDynamic",
      "label": "Lookup Table Dynamic",
      "icon": "lookupDynamic.svg",
      "phases": [
        "ALGEBRAIC"
      ],
      "width": 90,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 20,
          "side": "left"
        },
        {
          "x": 0,
          "y": 40,
          "side": "left"
        },
        {
          "x": 0,
          "y": 60,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 90,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {},
      "render": {
        "type": "image",
        "src": "exports/lookupDynamic.svg",
        "svgMode": "element",
        "preserveAspectRatio": "none",
        "x": 0,
        "y": 0,
        "width": 90,
        "height": 80
      }
    }
  ]
}
Runtimemodules/nflow_blocks/src/cpp/lookup/lookup2D.cpp
//=============================================================================
// Copyright (c) 2016-present Allan CORNET (Nelson)
//=============================================================================
// This file is part of Nelson.
//=============================================================================
// LICENCE_BLOCK_BEGIN
// SPDX-License-Identifier: LGPL-3.0-or-later
// LICENCE_BLOCK_END
//=============================================================================
// lookup2D: 2-D lookup table. Two inputs (row coordinate on port 0, column
// coordinate on port 1) index a static matrix Table stored column-major:
// Table(i,j) at index i + j*R (R = number of row breakpoints). Bilinear /
// Flat / Nearest interpolation with Clip / Linear extrapolation, element-wise
// with scalar expansion. ALGEBRAIC feedthrough, real double, no state.
//
// The interpolation is expressed with a per-axis weight w in [0,1] (Flat -> 0,
// Nearest -> round(f), Linear -> f); the output is the four-corner blend
//   T(i,j)(1-w1)(1-w2) + T(i+1,j) w1 (1-w2) + T(i,j+1)(1-w1) w2 + T(i+1,j+1) w1 w2
// which reduces to a single corner for Flat / Nearest.
//=============================================================================
#include "SimEngineTypes.hpp"
#include "BlockRegistry.hpp"
#include "FieldNames.hpp"
#include "NFlowBlockDescriptor.hpp"
#include "lookup_blocks.hpp"
#include "lookup_spline.hpp"
#include <string>
#include <vector>
//=============================================================================
namespace Nelson {
namespace NFlow {
    //=============================================================================
    namespace {
        // Locate the lower index i and raw fraction f for value u on a strictly
        // increasing axis. Clip holds f in [0,1] at the edges; Linear lets f run
        // past the edge segments so the caller extrapolates.
        void
        axisLocate(const std::vector<double>& bp, double u, bool linearExtrap, int& i, double& f)
        {
            const int n = (int)bp.size();
            if (u <= bp[0]) {
                i = 0;
                f = linearExtrap ? (u - bp[0]) / (bp[1] - bp[0]) : 0.0;
                return;
            }
            if (u >= bp[n - 1]) {
                i = n - 2;
                f = linearExtrap ? (u - bp[n - 2]) / (bp[n - 1] - bp[n - 2]) : 1.0;
                return;
            }
            i = n - 2;
            while (i > 0 && u < bp[i]) {
                --i;
            }
            f = (u - bp[i]) / (bp[i + 1] - bp[i]);
        }

        // Per-axis interpolation weight from the raw fraction f.
        double
        axisWeight(double f, const std::string& interp)
        {
            if (interp == "Flat") {
                return 0.0;
            }
            if (interp == "Nearest") {
                return (f < 0.5) ? 0.0 : 1.0;
            }
            return f; // Linear point-slope / Linear Lagrange
        }

        bool
        isStrictlyIncreasing(const std::vector<double>& v)
        {
            for (size_t i = 1; i < v.size(); ++i) {
                if (!(v[i] > v[i - 1])) {
                    return false;
                }
            }
            return true;
        }

        // Parse via BlockDescriptor so JSON arrays and string expressions both
        // resolve (matching the simulator / inspector-edited params).
        std::vector<double>
        paramVec(const BlockCodegenArgs& a, const char* key)
        {
            if (!a.block || !a.variables) {
                return {};
            }
            nflow::BlockDescriptor bd(*a.block, *a.variables);
            return bd.paramList(key);
        }

        std::string
        emitArray(const BlockCodegenArgs& a, const std::string& name, const std::vector<double>& v,
            bool rust)
        {
            std::string s
                = rust ? "let " + name + " = [" : "static const double " + name + "[] = {";
            for (size_t i = 0; i < v.size(); ++i) {
                s += (i ? ", " : "") + a.fmt(v[i]);
            }
            s += rust ? "];" : "};";
            return s;
        }

        // Emit the per-axis (index, fraction) locate for a strictly increasing
        // breakpoint array of length n, into integer var `iv` and double var
        // `fv`. `linExtrap` extends the edge segment's slope; otherwise clip.
        void
        emitLocate(const BlockCodegenArgs& a, bool rust, const std::string& arr,
            const std::string& uv, const std::string& iv, const std::string& fv, int n,
            bool linExtrap)
        {
            const std::string N1 = std::to_string(n - 1);
            const std::string N2 = std::to_string(n - 2);
            const std::string belowF = linExtrap
                ? "(" + uv + " - " + arr + "[0]) / (" + arr + "[1] - " + arr + "[0])"
                : (rust ? "0.0_f64" : "0.0");
            const std::string aboveF = linExtrap ? "(" + uv + " - " + arr + "[" + N2 + "]) / ("
                    + arr + "[" + N1 + "] - " + arr + "[" + N2 + "])"
                                                 : (rust ? "1.0_f64" : "1.0");
            if (rust) {
                a.line("let " + iv + ": usize; let " + fv + ": f64;");
                a.line("if " + uv + " <= " + arr + "[0] { " + iv + " = 0; " + fv + " = " + belowF
                    + "; }");
                a.line("else if " + uv + " >= " + arr + "[" + N1 + "] { " + iv + " = " + N2 + "; "
                    + fv + " = " + aboveF + "; }");
                a.line("else { let mut __k = " + N2 + "usize; while __k > 0 && " + uv + " < " + arr
                    + "[__k] { __k -= 1; } " + iv + " = __k; " + fv + " = (" + uv + " - " + arr
                    + "[__k]) / (" + arr + "[__k+1] - " + arr + "[__k]); }");
            } else {
                a.line("int " + iv + "; double " + fv + ";");
                a.line("if (" + uv + " <= " + arr + "[0]) { " + iv + " = 0; " + fv + " = " + belowF
                    + "; }");
                a.line("else if (" + uv + " >= " + arr + "[" + N1 + "]) { " + iv + " = " + N2 + "; "
                    + fv + " = " + aboveF + "; }");
                a.line("else { " + iv + " = " + N2 + "; while (" + iv + " > 0 && " + uv + " < "
                    + arr + "[" + iv + "]) " + iv + "--; " + fv + " = (" + uv + " - " + arr + "["
                    + iv + "]) / (" + arr + "[" + iv + "+1] - " + arr + "[" + iv + "]); }");
            }
        }

        std::string
        weightExpr(const std::string& fv, const std::string& interp, bool rust)
        {
            if (interp == "Flat") {
                return rust ? "0.0_f64" : "0.0";
            }
            if (interp == "Nearest") {
                return rust ? "(if " + fv + " < 0.5 { 0.0 } else { 1.0 })"
                            : "((" + fv + " < 0.5) ? 0.0 : 1.0)";
            }
            return fv;
        }

        void
        emitLookup2D(const BlockCodegenArgs& a, bool rust)
        {
            const std::vector<double> bp1 = paramVec(a, "BreakpointsForDimension1");
            const std::vector<double> bp2 = paramVec(a, "BreakpointsForDimension2");
            const std::vector<double> tbl = paramVec(a, "Table");
            nflow::BlockDescriptor bdp(*a.block, *a.variables);
            const std::string interp = bdp.paramStr("InterpMethod", "Linear point-slope");
            const bool linExtrap = (bdp.paramStr("ExtrapMethod", "Linear") == "Linear");
            const int R = (int)bp1.size();
            const int C = (int)bp2.size();
            if (R < 2 || C < 2 || (int)tbl.size() != R * C) {
                a.line("out_" + a.id + (rust ? " = 0.0_f64;" : " = 0.0;"));
                return;
            }
            if (lookupspline::isSpline(interp)) {
                // Separable tensor-product spline: dimension-1 coefficients are
                // baked per table column; the dimension-2 pass solves its dense
                // system at run time through the shared generated helpers.
                lookupspline::emitSplineLookupCodegen(
                    a, rust, { bp1, bp2 }, tbl, interp, bdp.paramStr("ExtrapMethod", "Linear"));
                return;
            }
            const std::string bp1v = "__lu2_bp1_" + a.id;
            const std::string bp2v = "__lu2_bp2_" + a.id;
            const std::string tv = "__lu2_tbl_" + a.id;
            a.line(emitArray(a, bp1v, bp1, rust));
            a.line(emitArray(a, bp2v, bp2, rust));
            a.line(emitArray(a, tv, tbl, rust));
            const std::string u1 = "__u1_" + a.id, u2 = "__u2_" + a.id;
            const std::string i1 = "__i1_" + a.id, i2 = "__j1_" + a.id;
            const std::string f1 = "__f1_" + a.id, f2 = "__f2_" + a.id;
            a.line((rust ? "let " : "double ") + u1 + " = " + a.in[0] + ";");
            a.line((rust ? "let " : "double ") + u2 + " = " + a.in[1] + ";");
            emitLocate(a, rust, bp1v, u1, i1, f1, R, linExtrap);
            emitLocate(a, rust, bp2v, u2, i2, f2, C, linExtrap);
            const std::string w1 = "__w1_" + a.id, w2 = "__w2_" + a.id;
            a.line((rust ? "let " : "double ") + w1 + " = " + weightExpr(f1, interp, rust) + ";");
            a.line((rust ? "let " : "double ") + w2 + " = " + weightExpr(f2, interp, rust) + ";");
            // T(ii,jj) = tv[ii + jj*R]
            const std::string Rs = std::to_string(R);
            auto T = [&](const std::string& ii, const std::string& jj) {
                return tv + "[" + ii + " + (" + jj + ") * " + Rs + "]";
            };
            const std::string ip1 = i1 + "+1", jp1 = i2 + "+1";
            const std::string blend = T(i1, i2) + " * (1.0 - " + w1 + ") * (1.0 - " + w2 + ") + "
                + T(ip1, i2) + " * " + w1 + " * (1.0 - " + w2 + ") + " + T(i1, jp1) + " * (1.0 - "
                + w1 + ") * " + w2 + " + " + T(ip1, jp1) + " * " + w1 + " * " + w2;
            a.line("out_" + a.id + " = " + blend + ";");
        }

        double
        interp2d(const std::vector<double>& bp1, const std::vector<double>& bp2,
            const std::vector<double>& tbl, double u1, double u2, const std::string& interp,
            bool linExtrap, const std::string& extrap)
        {
            const int R = (int)bp1.size();
            // Separable tensor-product spline: spline along dim1 for every dim2
            // column, then spline along dim2 through the intermediate values.
            if (lookupspline::isSpline(interp)) {
                const int C = (int)bp2.size();
                std::vector<double> v((size_t)C, 0.0);
                std::vector<double> col((size_t)R, 0.0);
                for (int jj = 0; jj < C; ++jj) {
                    for (int ii = 0; ii < R; ++ii) {
                        col[(size_t)ii] = tbl[(size_t)ii + (size_t)jj * (size_t)R];
                    }
                    v[(size_t)jj] = lookupspline::spline1DValue(bp1, col, u1, interp, extrap);
                }
                return lookupspline::spline1DValue(bp2, v, u2, interp, extrap);
            }
            int i = 0, j = 0;
            double f1 = 0.0, f2 = 0.0;
            axisLocate(bp1, u1, linExtrap, i, f1);
            axisLocate(bp2, u2, linExtrap, j, f2);
            const double w1 = axisWeight(f1, interp);
            const double w2 = axisWeight(f2, interp);
            auto T = [&](int ii, int jj) { return tbl[(size_t)ii + (size_t)jj * (size_t)R]; };
            return T(i, j) * (1.0 - w1) * (1.0 - w2) + T(i + 1, j) * w1 * (1.0 - w2)
                + T(i, j + 1) * (1.0 - w1) * w2 + T(i + 1, j + 1) * w1 * w2;
        }
    } // namespace
    //=============================================================================
    bool
    handleLookup2D(SimCtx& ctx, const Block& b, Phase phase)
    {
        if (phase != Phase::ALGEBRAIC) {
            return false;
        }
        if (!hasInput(ctx, b.nid, 0) || !hasInput(ctx, b.nid, 1)) {
            return false;
        }
        nflow::BlockDescriptor bd(b, ctx.variables);
        const std::vector<double> bp1 = bd.paramList("BreakpointsForDimension1");
        const std::vector<double> bp2 = bd.paramList("BreakpointsForDimension2");
        const std::vector<double> tbl = bd.paramList("Table");
        const std::string interp = bd.paramStr("InterpMethod", "Linear point-slope");
        const std::string extrap = bd.paramStr("ExtrapMethod", "Linear");
        const bool linExtrap = (extrap == "Linear");
        SigView u1 = getInputSig(ctx, b.nid, 0);
        SigView u2 = getInputSig(ctx, b.nid, 1);
        return emitElementwise(ctx, b.nid, [&](int k) {
            return interp2d(bp1, bp2, tbl, sigAt(u1, k), sigAt(u2, k), interp, linExtrap, extrap);
        });
    }
    //=============================================================================
    bool
    resolveLookup2DDims(const Block& b, const ValMap& vars, const std::vector<PortSig>& inSigs,
        std::vector<PortSig>& outSigs, std::string& err)
    {
        nflow::BlockDescriptor bd(b, vars);
        const std::vector<double> bp1 = bd.paramList("BreakpointsForDimension1");
        const std::vector<double> bp2 = bd.paramList("BreakpointsForDimension2");
        const std::vector<double> tbl = bd.paramList("Table");
        if (bp1.size() < 2 || bp2.size() < 2) {
            err = "lookup2D requires at least 2 breakpoints per dimension";
            return false;
        }
        if (tbl.size() != bp1.size() * bp2.size()) {
            err = "lookup2D table size must be rows*cols";
            return false;
        }
        if (!isStrictlyIncreasing(bp1) || !isStrictlyIncreasing(bp2)) {
            err = "lookup2D breakpoints must be strictly increasing";
            return false;
        }
        // Output width follows the wider input (element-wise pairing).
        int w = 1;
        for (const auto& s : inSigs) {
            w = std::max(w, s.width());
        }
        outSigs[0].setVector(w);
        outSigs[0].type = SigType::Double;
        return true;
    }
    //=============================================================================
    // Shared spline helpers, emitted once per target when any lookup block in
    // the model interpolates with a spline (same `once` key as lookupND).
    static void
    emitLookup2DShared(const BlockCodegenStateArgs& a, bool rust)
    {
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        if (!lookupspline::isSpline(bd.paramStr("InterpMethod", "Linear point-slope"))) {
            return;
        }
        if (!a.once("nflow:spline:helpers")) {
            return;
        }
        lookupspline::emitSplineCodegenHelpers(a, rust);
    }
    //=============================================================================
    BlockCodegenTemplate
    getCodeGenCLookup2D()
    {
        BlockCodegenTemplate t;
        t.emitStep = [](const BlockCodegenArgs& a) { emitLookup2D(a, false); };
        t.emitShared = [](const BlockCodegenStateArgs& a) { emitLookup2DShared(a, false); };
        return t;
    }
    //=============================================================================
    BlockCodegenTemplate
    getCodeGenRustLookup2D()
    {
        BlockCodegenTemplate t;
        t.emitStep = [](const BlockCodegenArgs& a) { emitLookup2D(a, true); };
        t.emitShared = [](const BlockCodegenStateArgs& a) { emitLookup2DShared(a, true); };
        return t;
    }
    //=============================================================================
} // namespace NFlow
} // namespace Nelson
//=============================================================================
🔗See Also
lookup1D
🕔Version History
Version Description
1.0.0 initial version
Edit this page on GitHub