tf
Implements a continuous transfer function approximation.
📝Syntax
Block type: tf
📥Input Arguments
Parameter Description
input ports 1 input port(s) declared.
📤Output Arguments
Parameter Description
output ports 1 output port(s) declared.
📄Description

Implements a continuous transfer function approximation.

Module nflow_blocks
Library Continuous blocks
Type tf
Label Transfer Fn

Description

A continuous-time transfer function block defined by numerator and denominator polynomials. Useful for representing linear dynamics in the Laplace domain.

Ports

Input(s)

Port Role Side Position
Port_1 Numeric signal read by the block. left x=0, y=40

Output(s)

Port Role Side Position
Port_1 Numeric signal produced by the block. right x=85, y=40

Parameters

Parameter Default value
num [3]
den [1, 3]

Inspector Keys

These serialized keys are exposed by the block inspector.

Block Characteristics

Block type tf
Family Continuous blocks
Rendered size 85 x 80
Phases INIT, OUTPUT, ALGEBRAIC, UPDATE
Direct feedthrough yes
Internal state or history yes
Signal data type double numeric values

Algorithms

Equation or Rule

$$y \approx \frac{num(s)}{den(s)}\,u$$

Extended Capabilities

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/continuous/library.json
{
  "id": "builtin.continuous",
  "title": "Continuous",
  "version": "1.0.0",
  "format": "nflow-2",
  "metadata": {
    "author": "Allan CORNET",
    "created": "2026-03-21",
    "tool": "Nelson nflow"
  },
  "comment": "Blocks for continuous-time systems",
  "license": "LGPL-3.0",
  "builtin": true,
  "blocks": [
    {
      "type": "integrator",
      "label": "Integrator",
      "icon": "integrator.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "InitialCondition": 0,
        "ExternalReset": "none",
        "InitialConditionSource": "internal",
        "LowerSaturationLimit": "-inf",
        "UpperSaturationLimit": "inf"
      },
      "render": {
        "type": "math",
        "useRectElement": true,
        "bodyClass": "block-body integrator-body",
        "mathGroupClass": "integrator-math",
        "formula": "\\frac{1}{s}"
      }
    },
    {
      "type": "tf",
      "label": "Transfer Fn",
      "icon": "tf.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "ALGEBRAIC",
        "UPDATE"
      ],
      "width": 85,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 85,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "Numerator": [
          3
        ],
        "Denominator": [
          1,
          3
        ]
      },
      "render": {
        "type": "math",
        "bodyClass": "block-body",
        "mathGroupClass": "tf-math",
        "formula": "\\frac{N(s)}{D(s)}"
      }
    },
    {
      "type": "delay",
      "label": "Delay",
      "icon": "delay.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "DelayTime": 0.1
      },
      "render": {
        "type": "math",
        "bodyClass": "block-body",
        "mathGroupClass": "delay-math",
        "formula": "e^{-sT}"
      }
    },
    {
      "type": "stateSpace",
      "label": "State-Space",
      "icon": "stateSpace.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 160,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 160,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "A": 1,
        "B": 1,
        "C": 1,
        "D": 0,
        "InitialCondition": 0
      },
      "render": {
        "type": "math",
        "bodyClass": "block-body",
        "mathGroupClass": "state-space-math",
        "formula": "\\dot{x}=Ax+Bu",
        "textSize": "16px"
      }
    },
    {
      "type": "lpf",
      "label": "LPF",
      "icon": "lpf.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "Cutoff": 1
      },
      "render": {
        "src": "lpf.svg",
        "type": "image"
      }
    },
    {
      "type": "hpf",
      "label": "HPF",
      "icon": "hpf.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "Cutoff": 1
      },
      "render": {
        "src": "hpf.svg",
        "type": "image"
      }
    },
    {
      "type": "derivative",
      "label": "Derivative",
      "icon": "derivative.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {},
      "render": {
        "type": "math",
        "useRectElement": true,
        "bodyClass": "block-body",
        "mathGroupClass": "derivative-math",
        "formula": "\\frac{d}{dt}"
      }
    },
    {
      "type": "pid",
      "label": "PID",
      "icon": "pid.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "UPDATE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "P": 1,
        "I": 0,
        "D": 0,
        "N": 0,
        "LowerSaturationLimit": "-inf",
        "UpperSaturationLimit": "inf"
      },
      "render": {
        "type": "math",
        "bodyClass": "block-body",
        "mathGroupClass": "pid-math",
        "formula": "\\mathsf{PID}"
      }
    },
    {
      "type": "constraint",
      "label": "Constraint",
      "icon": "constraint.svg",
      "phases": [
        "INIT",
        "OUTPUT",
        "DERIVATIVE"
      ],
      "width": 80,
      "height": 80,
      "inputs": [
        {
          "x": 0,
          "y": 40,
          "side": "left"
        }
      ],
      "outputs": [
        {
          "x": 80,
          "y": 40,
          "side": "right"
        }
      ],
      "defaultParams": {
        "InitialCondition": 0
      },
      "render": {
        "type": "math",
        "bodyClass": "block-body",
        "mathGroupClass": "constraint-math",
        "formula": "g=0"
      }
    }
  ]
}
Runtimemodules/nflow_blocks/src/cpp/continuous/tf.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
//=============================================================================
#include "SimEngineTypes.hpp"
#include "BlockRegistry.hpp"
#include "FieldNames.hpp"
#include "NFlowBlockDescriptor.hpp"
#include "NFlowCodegenPolynomial.hpp"
#include "NFlowCodegenHelpers.hpp"
#include <cmath>
#include <algorithm>
#include "continuous_blocks.hpp"
//=============================================================================
bool
Nelson::NFlow::handleTf(SimCtx& ctx, const Block& b, Phase phase)
{
    auto& st = getState(ctx, b.nid);
    const int w = std::max(1, outputWidth(ctx, b.nid, 0));
    // Vector tf: each element is an independent SISO transfer function with the
    // SAME numerator/denominator. st.xHist (A_last row) and st.yHist (C vector)
    // have length n and are shared; st.tfState is a flat [w * n] state array,
    // element e occupying [e*n, e*n+n).
    if (phase == Phase::INIT) {
        int n = 0;
        std::vector<double> state1;
        bool ok = buildTfState(b.params.value(nflow::kNum, json::array()),
            b.params.value(nflow::kDen, json::array()), st.xHist, st.yHist, st.tfD, state1, n,
            ctx.variables);
        if (!ok) {
            st.tfState.clear();
            st.xHist.clear();
            st.yHist.clear();
            st.tfD = 0.0;
        } else {
            st.tfState.assign((size_t)n * w, 0.0); // n==0 -> empty (pure gain)
        }
        st.output = 0.0;
        return false;
    }
    const int n = (int)st.xHist.size(); // per-element state order

    if (phase == Phase::OUTPUT) {
        SigView u = getInputSig(ctx, b.nid, 0);
        double* xs = blockX(ctx, b.nid);
        double* y = outputSlice(ctx, b.nid, 0);
        std::vector<double> x(std::max(1, n));
        for (int e = 0; e < w; ++e) {
            const double inp = sigAt(u, e);
            if (n == 0) {
                y[e] = st.tfD * inp;
                continue;
            }
            const double* src = xs ? (xs + (size_t)e * n) : (st.tfState.data() + (size_t)e * n);
            for (int i = 0; i < n; ++i) {
                x[i] = src[i];
            }
            x.resize(n);
            y[e] = tfOutput(st.yHist, x, st.tfD, inp);
        }
        return false;
    }
    if (phase == Phase::ALGEBRAIC) {
        if (n != 0) {
            return false; // has state: handled in OUTPUT/UPDATE
        }
        // Pure gain (n == 0): element-wise D*u.
        SigView u = getInputSig(ctx, b.nid, 0);
        return emitElementwise(ctx, b.nid, [&](int e) { return st.tfD * sigAt(u, e); });
    }
    if (phase == Phase::UPDATE) {
        if (n == 0) {
            return false;
        }
        SigView u = getInputSig(ctx, b.nid, 0);
        std::vector<double> slice(n);
        for (int e = 0; e < w; ++e) {
            double* base = st.tfState.data() + (size_t)e * n;
            for (int i = 0; i < n; ++i) {
                slice[i] = base[i];
            }
            std::vector<double> next = tfRK4(st.xHist, slice, sigAt(u, e), ctx.dt);
            for (int i = 0; i < n && i < (int)next.size(); ++i) {
                base[i] = next[i];
            }
        }
        return false;
    }
    if (phase == Phase::DERIVATIVE) {
        // Controller-canonical-form derivative; st.xHist holds the companion
        // last row. Shares tfDerivative() with tfRK4 so the two cannot drift.
        double* xdot = blockXdot(ctx, b.nid);
        if (xdot && n != 0) {
            double* xs = blockX(ctx, b.nid);
            SigView u = getInputSig(ctx, b.nid, 0);
            std::vector<double> x(n), dx;
            for (int e = 0; e < w; ++e) {
                const double* src = xs ? (xs + (size_t)e * n) : (st.tfState.data() + (size_t)e * n);
                for (int i = 0; i < n; ++i) {
                    x[i] = src[i];
                }
                tfDerivative(st.xHist, x, sigAt(u, e), dx);
                for (int i = 0; i < n && i < (int)dx.size(); ++i) {
                    xdot[(size_t)e * n + i] = dx[i];
                }
            }
        }
        return false;
    }
    return false;
}
//=============================================================================
// Continuous state order n of a transfer function: (trimmed denominator degree).
// n == 0 is a pure algebraic gain (y = D*u, no state). Shared by the RK4 codegen
// layout so it agrees with the companion form built in emitShared.
static int
tfStateOrder(const std::vector<double>& denIn)
{
    std::vector<double> den = denIn.empty() ? std::vector<double> { 1.0 } : denIn;
    size_t i = 0;
    while (i + 1 < den.size() && std::abs(den[i]) < 1e-12) {
        ++i;
    }
    int n = static_cast<int>(den.size() - i) - 1;
    return n <= 0 ? 0 : n;
}
//=============================================================================
Nelson::NFlow::BlockCodegenTemplate
Nelson::NFlow::getCodeGenCTf()
{
    BlockCodegenTemplate t;
    t.sharedPriority = 1;
    t.emitState = [](const BlockCodegenStateArgs& a) {
        a.declState("double tf_" + a.id + "[MAX_DIM];");
        a.addInit("  for (int i = 0; i < MAX_DIM; i++) s->tf_" + a.id + "[i] = 0.0;");
    };
    t.emitStep = [](const BlockCodegenArgs& a) {
        a.line("out_" + a.id + " = tf_step(&tf_params_" + a.id + ", s->tf_" + a.id + ", " + a.in[0]
            + ", dt);");
    };
    // Unified variable-step codegen: a tf contributes n continuous
    // states (companion form). n == 0 is a pure gain (algebraic, no state): safe
    // to keep its emitStep in the rhs() body, so report 0, not a fallback.
    t.continuousWidth = [](const BlockCodegenArgs& a) -> int {
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        return tfStateOrder(bd.paramList(nflow::kDen));
    };
    // Output y = D*u + C.x and derivative x' = A_companion.x + B.u, reusing the
    // shared tf_out / tf_deriv helpers over the scattered state s->tf_<id>.
    t.emitRk4 = [](const BlockCodegenArgs& a) {
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        const int n = tfStateOrder(bd.paramList(nflow::kDen));
        const std::string off = std::to_string(a.stateOffset);
        const std::string nStr = std::to_string(n);
        switch (a.rk4Op) {
        case Rk4Gather:
            a.line("for (int __i = 0; __i < " + nStr + "; ++__i) " + a.rk4Arr + "[" + off
                + " + __i] = s->tf_" + a.id + "[__i];");
            break;
        case Rk4Scatter:
            a.line("for (int __i = 0; __i < " + nStr + "; ++__i) s->tf_" + a.id
                + "[__i] = " + a.rk4Arr + "[" + off + " + __i];");
            break;
        case Rk4Output:
            a.line("out_" + a.id + " = tf_out(&tf_params_" + a.id + ", s->tf_" + a.id + ", "
                + a.in[0] + ");");
            break;
        case Rk4Deriv:
            a.line("tf_deriv(tf_params_" + a.id + ".n, tf_params_" + a.id + ".A, s->tf_" + a.id
                + ", " + a.in[0] + ", &" + a.rk4Arr + "[" + off + "]);");
            break;
        default:
            break;
        }
    };
    t.emitShared = [](const BlockCodegenStateArgs& a) {
        // Companion-form RK4 stepper matching the interpreter (y = D*u + C.x,
        // then classical RK4 on x' = A_companion.x + B.u). Replaces the former
        // Tustin direct-form discretization so generated code and interactive
        // simulation agree numerically.
        auto companion = [](const std::vector<double>& numIn, const std::vector<double>& denIn,
                             int& n, double& D, std::vector<double>& C, std::vector<double>& A) {
            auto trim = [](std::vector<double> v) {
                size_t i = 0;
                while (i + 1 < v.size() && std::abs(v[i]) < 1e-12) {
                    ++i;
                }
                return std::vector<double>(v.begin() + i, v.end());
            };
            std::vector<double> num = trim(numIn.empty() ? std::vector<double> { 0.0 } : numIn);
            std::vector<double> den = trim(denIn.empty() ? std::vector<double> { 1.0 } : denIn);
            double a0 = den[0];
            for (double& v : den) {
                v /= a0;
            }
            for (double& v : num) {
                v /= a0;
            }
            n = static_cast<int>(den.size()) - 1;
            if (n <= 0) {
                n = 0;
                D = num.empty() ? 0.0 : num[0];
                C.clear();
                A.clear();
                return;
            }
            while (static_cast<int>(num.size()) < n + 1) {
                num.insert(num.begin(), 0.0);
            }
            std::vector<double> aTail(den.begin() + 1, den.end());
            D = num[0];
            C.assign(n, 0.0);
            for (int i = 0; i < n; ++i) {
                C[n - 1 - i] = num[i + 1] - aTail[i] * D;
            }
            A.assign(n, 0.0);
            for (int j = 0; j < n; ++j) {
                A[j] = -aTail[n - 1 - j];
            }
        };

        int maxTfOrder = 1;
        for (const auto& b : *a.allBlocks) {
            if (nflow::jstr(b, nflow::kType) != "tf") {
                continue;
            }
            nflow::BlockDescriptor tbd(b, *a.variables);
            int tn = 0;
            double tD = 0.0;
            std::vector<double> tC, tA;
            companion(tbd.paramList(nflow::kNum), tbd.paramList(nflow::kDen), tn, tD, tC, tA);
            maxTfOrder = std::max(maxTfOrder, tn);
        }
        if (a.once("tf:typedefs")) {
            a.addConst("#define MAX_DIM " + std::to_string(maxTfOrder));
            a.addConst("typedef struct { int n; double D; double C[MAX_DIM]; double A[MAX_DIM]; } "
                       "TfParams;");
            a.addHelper("static void tf_deriv(int n, const double* A, const double* x, double u, "
                        "double* dx) {");
            a.addHelper("  for (int i = 0; i < n - 1; i++) dx[i] = x[i + 1];");
            a.addHelper("  double last = u;");
            a.addHelper("  for (int j = 0; j < n; j++) last += A[j] * x[j];");
            a.addHelper("  dx[n - 1] = last;");
            a.addHelper("}");
            // Output-only y = D*u + C.x (no state advance): used by the unified
            // RK4 rhs() so the global stepper owns the integration.
            a.addHelper("static double tf_out(const TfParams* p, const double* x, double u) {");
            a.addHelper("  double y = p->D * u;");
            a.addHelper("  for (int i = 0; i < p->n; i++) y += p->C[i] * x[i];");
            a.addHelper("  return y;");
            a.addHelper("}");
            a.addHelper(
                "static double tf_step(const TfParams* p, double* x, double u, double dt) {");
            a.addHelper("  const int n = p->n;");
            a.addHelper("  double y = p->D * u;");
            a.addHelper("  for (int i = 0; i < n; i++) y += p->C[i] * x[i];");
            a.addHelper("  if (n > 0) {");
            a.addHelper(
                "    double k1[MAX_DIM], k2[MAX_DIM], k3[MAX_DIM], k4[MAX_DIM], tmp[MAX_DIM];");
            a.addHelper("    tf_deriv(n, p->A, x, u, k1);");
            a.addHelper("    for (int i = 0; i < n; i++) tmp[i] = x[i] + 0.5 * dt * k1[i];");
            a.addHelper("    tf_deriv(n, p->A, tmp, u, k2);");
            a.addHelper("    for (int i = 0; i < n; i++) tmp[i] = x[i] + 0.5 * dt * k2[i];");
            a.addHelper("    tf_deriv(n, p->A, tmp, u, k3);");
            a.addHelper("    for (int i = 0; i < n; i++) tmp[i] = x[i] + dt * k3[i];");
            a.addHelper("    tf_deriv(n, p->A, tmp, u, k4);");
            a.addHelper("    for (int i = 0; i < n; i++)");
            a.addHelper("      x[i] += (dt / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);");
            a.addHelper("  }");
            a.addHelper("  return y;");
            a.addHelper("}");
            a.addHelper("");
        }
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        int n = 0;
        double D = 0.0;
        std::vector<double> C, A;
        companion(bd.paramList(nflow::kNum), bd.paramList(nflow::kDen), n, D, C, A);
        std::vector<double> cPad(maxTfOrder, 0.0), aPad(maxTfOrder, 0.0);
        for (int i = 0; i < n; ++i) {
            cPad[i] = C[i];
            aPad[i] = A[i];
        }
        std::string cStr, aStr;
        for (int i = 0; i < maxTfOrder; ++i) {
            if (i > 0) {
                cStr += ", ";
                aStr += ", ";
            }
            cStr += a.fmt(cPad[i]);
            aStr += a.fmt(aPad[i]);
        }
        a.addConst("static const TfParams tf_params_" + a.id + " = { " + std::to_string(n) + ", "
            + a.fmt(D) + ", {" + cStr + "}, {" + aStr + "} };");
    };
    return t;
}
//=============================================================================
Nelson::NFlow::BlockCodegenTemplate
Nelson::NFlow::getCodeGenRustTf()
{
    BlockCodegenTemplate t;
    t.sharedPriority = 1;
    t.emitStep = [](const BlockCodegenArgs& a) {
        std::string cu = a.id;
        for (auto& c : cu) {
            c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
        }
        a.line("out_" + a.id + " = tf_step(TF_N_" + cu + ", TF_D_" + cu + ", &TF_C_" + cu
            + ", &TF_A_" + cu + ", &mut s.tf_x_" + a.id + ", " + a.in[0] + ", dt);");
    };
    // Unified variable-step codegen, Rust backend: n companion
    // states integrated through the shared rhs (tf_out / tf_deriv). n == 0 is a
    // pure algebraic gain (no state).
    t.continuousWidth = [](const BlockCodegenArgs& a) -> int {
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        return tfStateOrder(bd.paramList(nflow::kDen));
    };
    t.emitRk4 = [](const BlockCodegenArgs& a) {
        nflow::BlockDescriptor bd(*a.block, *a.variables);
        const int n = tfStateOrder(bd.paramList(nflow::kDen));
        std::string cu = a.id;
        for (auto& c : cu) {
            c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
        }
        const std::string off = std::to_string(a.stateOffset);
        const std::string nc = "TF_N_" + cu;
        switch (a.rk4Op) {
        case Rk4Gather:
            a.line("for __i in 0.." + nc + " { " + a.rk4Arr + "[" + off + " + __i] = s.tf_x_" + a.id
                + "[__i]; }");
            break;
        case Rk4Scatter:
            a.line("for __i in 0.." + nc + " { s.tf_x_" + a.id + "[__i] = " + a.rk4Arr + "[" + off
                + " + __i]; }");
            break;
        case Rk4Output:
            a.line("out_" + a.id + " = tf_out(" + nc + ", TF_D_" + cu + ", &TF_C_" + cu
                + ", &s.tf_x_" + a.id + ", " + a.in[0] + ");");
            break;
        case Rk4Deriv:
            a.line("tf_deriv(" + nc + ", &TF_A_" + cu + ", &s.tf_x_" + a.id + ", " + a.in[0]
                + ", &mut " + a.rk4Arr + "[" + off + "..(" + off + " + " + nc + ")]);");
            break;
        default:
            break;
        }
        (void)n;
    };
    t.emitShared = [](const BlockCodegenStateArgs& a) {
        // Companion-form RK4, matching the interpreter (see the C generator).
        auto companion = [](const std::vector<double>& numIn, const std::vector<double>& denIn,
                             int& n, double& D, std::vector<double>& C, std::vector<double>& A) {
            auto trim = [](std::vector<double> v) {
                size_t i = 0;
                while (i + 1 < v.size() && std::abs(v[i]) < 1e-12) {
                    ++i;
                }
                return std::vector<double>(v.begin() + i, v.end());
            };
            std::vector<double> num = trim(numIn.empty() ? std::vector<double> { 0.0 } : numIn);
            std::vector<double> den = trim(denIn.empty() ? std::vector<double> { 1.0 } : denIn);
            double a0 = den[0];
            for (double& v : den) {
                v /= a0;
            }
            for (double& v : num) {
                v /= a0;
            }
            n = static_cast<int>(den.size()) - 1;
            if (n <= 0) {
                n = 0;
                D = num.empty() ? 0.0 : num[0];
                C.clear();
                A.clear();
                return;
            }
            while (static_cast<int>(num.size()) < n + 1) {
                num.insert(num.begin(), 0.0);
            }
            std::vector<double> aTail(den.begin() + 1, den.end());
            D = num[0];
            C.assign(n, 0.0);
            for (int i = 0; i < n; ++i) {
                C[n - 1 - i] = num[i + 1] - aTail[i] * D;
            }
            A.assign(n, 0.0);
            for (int j = 0; j < n; ++j) {
                A[j] = -aTail[n - 1 - j];
            }
        };

        int maxTfOrder = 1;
        for (const auto& b : *a.allBlocks) {
            if (nflow::jstr(b, nflow::kType) != "tf") {
                continue;
            }
            nflow::BlockDescriptor tbd(b, *a.variables);
            int tn = 0;
            double tD = 0.0;
            std::vector<double> tC, tA;
            companion(tbd.paramList(nflow::kNum), tbd.paramList(nflow::kDen), tn, tD, tC, tA);
            maxTfOrder = std::max(maxTfOrder, tn);
        }
        a.declState("    pub tf_x_" + a.id + ": [f64; " + std::to_string(maxTfOrder) + "],");
        a.addInit(
            "    s.tf_x_" + a.id + " = [" + a.fmt(0.0) + "; " + std::to_string(maxTfOrder) + "];");

        nflow::BlockDescriptor bd(*a.block, *a.variables);
        int n = 0;
        double D = 0.0;
        std::vector<double> C, A;
        companion(bd.paramList(nflow::kNum), bd.paramList(nflow::kDen), n, D, C, A);
        std::vector<double> cPad(maxTfOrder, 0.0), aPad(maxTfOrder, 0.0);
        for (int i = 0; i < n; ++i) {
            cPad[i] = C[i];
            aPad[i] = A[i];
        }
        std::string cStr, aStr;
        for (int i = 0; i < maxTfOrder; ++i) {
            if (i > 0) {
                cStr += ", ";
                aStr += ", ";
            }
            cStr += a.fmt(cPad[i]);
            aStr += a.fmt(aPad[i]);
        }
        std::string cu = a.id;
        for (auto& c : cu) {
            c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
        }
        a.addConst("const TF_N_" + cu + ": usize = " + std::to_string(n) + ";");
        a.addConst("const TF_D_" + cu + ": f64 = " + a.fmt(D) + ";");
        a.addConst(
            "const TF_C_" + cu + ": [f64; " + std::to_string(maxTfOrder) + "] = [" + cStr + "];");
        a.addConst(
            "const TF_A_" + cu + ": [f64; " + std::to_string(maxTfOrder) + "] = [" + aStr + "];");
        if (a.once("tf:helper")) {
            a.addHelper("fn tf_deriv(n: usize, a: &[f64], x: &[f64], u: f64, dx: &mut [f64]) {");
            a.addHelper("    if n > 0 { for i in 0..n - 1 { dx[i] = x[i + 1]; } }");
            a.addHelper("    let mut last = u;");
            a.addHelper("    for j in 0..n { last += a[j] * x[j]; }");
            a.addHelper("    if n > 0 { dx[n - 1] = last; }");
            a.addHelper("}");
            // Output-only y = D*u + C.x for the unified RK4 rhs (no state advance).
            a.addHelper("#[allow(dead_code)]");
            a.addHelper("fn tf_out(n: usize, d: f64, c: &[f64], x: &[f64], u: f64) -> f64 {");
            a.addHelper("    let mut y = d * u;");
            a.addHelper("    for i in 0..n { y += c[i] * x[i]; }");
            a.addHelper("    y");
            a.addHelper("}");
            a.addHelper("#[allow(clippy::too_many_arguments)]");
            a.addHelper("fn tf_step(n: usize, d: f64, c: &[f64], a: &[f64],");
            a.addHelper("           x: &mut [f64], u: f64, dt: f64) -> f64 {");
            a.addHelper("    let mut y = d * u;");
            a.addHelper("    for i in 0..n { y += c[i] * x[i]; }");
            a.addHelper("    if n > 0 {");
            a.addHelper("        let (mut k1, mut k2) = ([0.0_f64; " + std::to_string(maxTfOrder)
                + "], [0.0_f64; " + std::to_string(maxTfOrder) + "]);");
            a.addHelper("        let (mut k3, mut k4) = ([0.0_f64; " + std::to_string(maxTfOrder)
                + "], [0.0_f64; " + std::to_string(maxTfOrder) + "]);");
            a.addHelper("        let mut tmp = [0.0_f64; " + std::to_string(maxTfOrder) + "];");
            a.addHelper("        tf_deriv(n, a, x, u, &mut k1);");
            a.addHelper("        for i in 0..n { tmp[i] = x[i] + 0.5 * dt * k1[i]; }");
            a.addHelper("        tf_deriv(n, a, &tmp, u, &mut k2);");
            a.addHelper("        for i in 0..n { tmp[i] = x[i] + 0.5 * dt * k2[i]; }");
            a.addHelper("        tf_deriv(n, a, &tmp, u, &mut k3);");
            a.addHelper("        for i in 0..n { tmp[i] = x[i] + dt * k3[i]; }");
            a.addHelper("        tf_deriv(n, a, &tmp, u, &mut k4);");
            a.addHelper("        for i in 0..n { x[i] += (dt / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * "
                        "k3[i] + k4[i]); }");
            a.addHelper("    }");
            a.addHelper("    y");
            a.addHelper("}");
            a.addHelper("");
        }
    };
    return t;
}
//=============================================================================
🔗See Also
stateSpaceintegratordtflpfhpf
🕔Version History
Version Description
1.0.0 initial version
Edit this page on GitHub