Outputs 1 on the step where the input crosses HitCrossingOffset.
Because the input is read in the OUTPUT phase, drive this block from a source rather than through an ALGEBRAIC feedthrough block (whose output would be one step stale).
Code generation: supported for C and Rust.
Manifestmodules/nflow_blocks/libraries/nonlinear/library.json
{
"id": "builtin.nonlinear",
"title": "Non-Linear",
"version": "1.0.0",
"format": "nflow-2",
"metadata": {
"author": "Allan CORNET",
"created": "2026-03-21",
"tool": "Nelson nflow"
},
"comment": "Blocks for non-linearities",
"license": "LGPL-3.0",
"builtin": true,
"blocks": [
{
"type": "saturation",
"icon": "saturation.svg",
"label": "Saturation",
"phases": [
"ALGEBRAIC"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"LowerLimit": -1,
"UpperLimit": 1
},
"render": {
"type": "image",
"src": "saturation.svg"
}
},
{
"type": "hysteresis",
"label": "Relay",
"icon": "hysteresis.svg",
"phases": [
"INIT",
"OUTPUT",
"UPDATE"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"uHigh": 1,
"uLow": -1,
"yHigh": 1,
"yLow": 0
},
"render": {
"type": "image",
"src": "hysteresis.svg"
}
},
{
"type": "rate",
"label": "Rate Lim.",
"icon": "rate.svg",
"phases": [
"INIT",
"OUTPUT",
"UPDATE"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"RisingSlewLimit": 1,
"FallingSlewLimit": 1
},
"render": {
"type": "image",
"src": "rate.svg"
}
},
{
"type": "backlash",
"label": "Backlash",
"icon": "backlash.svg",
"phases": [
"INIT",
"OUTPUT",
"UPDATE"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"BacklashWidth": 1
},
"render": {
"type": "image",
"src": "backlash.svg"
}
},
{
"type": "deadZone",
"label": "Dead Zone",
"icon": "deadZone.svg",
"phases": [
"ALGEBRAIC"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"LowerValue": -1,
"UpperValue": 1
},
"render": {
"type": "image",
"src": "deadZone.svg",
"svgMode": "element",
"preserveAspectRatio": "none",
"x": 0,
"y": 0,
"width": 80,
"height": 80
}
},
{
"type": "quantizer",
"label": "Quantizer",
"icon": "quantizer.svg",
"phases": [
"ALGEBRAIC"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"QuantizationInterval": 1
},
"render": {
"type": "image",
"src": "quantizer.svg",
"svgMode": "element",
"preserveAspectRatio": "none",
"x": 0,
"y": 0,
"width": 80,
"height": 80
}
},
{
"type": "hitCrossing",
"icon": "hitCrossing.svg",
"label": "Hit Crossing",
"phases": [
"INIT",
"OUTPUT",
"UPDATE"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"HitCrossingOffset": 0,
"HitCrossingDirection": "either"
},
"render": {
"type": "image",
"src": "hitCrossing.svg"
}
},
{
"type": "coulombViscousFriction",
"label": "Coulomb & Viscous Friction",
"icon": "coulombViscousFriction.svg",
"phases": [
"ALGEBRAIC"
],
"width": 80,
"height": 80,
"inputs": [
{
"x": 0,
"y": 40,
"side": "left"
}
],
"outputs": [
{
"x": 80,
"y": 40,
"side": "right"
}
],
"defaultParams": {
"Gain": 1,
"Offset": 1
},
"render": {
"type": "image",
"src": "coulombViscousFriction.svg",
"svgMode": "element",
"preserveAspectRatio": "none",
"x": 0,
"y": 0,
"width": 80,
"height": 80
}
}
]
}
Runtimemodules/nflow_blocks/src/cpp/nonlinear/hitCrossing.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
//=============================================================================
// hitCrossing: detects when the (scalar) input reaches HitCrossingOffset in the
// configured direction and outputs 1.0 on the step where the crossing happens,
// else 0.0. Direction is "rising", "falling" or "either" (default). Stateful:
// the previous input (relative to the offset) is held so a straddle can be
// detected. The first step has nothing to straddle, so it fires only when the
// input STARTS on the offset - which is a hit, and the only one that instant
// can report. C / Rust code generation.
//=============================================================================
#include "SimEngineTypes.hpp"
#include "BlockRegistry.hpp"
#include "FieldNames.hpp"
#include "NFlowBlockDescriptor.hpp"
#include <cmath>
#include <string>
#include "nonlinear_blocks.hpp"
//=============================================================================
namespace Nelson {
namespace NFlow {
//=============================================================================
// Direction codes: 0 = either, 1 = rising, 2 = falling.
static int
hcDirection(const nflow::BlockDescriptor& bd)
{
const std::string d = bd.paramStr("HitCrossingDirection", "either");
if (d == "rising") {
return 1;
}
if (d == "falling") {
return 2;
}
return 0;
}
//=============================================================================
static bool
hcHit(double prev, double cur, int dir)
{
const bool rising = (prev < 0.0 && cur >= 0.0);
const bool falling = (prev > 0.0 && cur <= 0.0);
if (dir == 1) {
return rising;
}
if (dir == 2) {
return falling;
}
return rising || falling;
}
//=============================================================================
bool
handleHitCrossing(SimCtx& ctx, const Block& b, Phase phase)
{
auto& st = getState(ctx, b.nid);
if (phase == Phase::INIT) {
st.scalar = 0.0; // previous input relative to the offset
st.scalar2 = 0.0; // primed flag (0 until the first step is seen)
st.output = 0.0;
return false;
}
nflow::BlockDescriptor bd(b, ctx.variables);
const double off = bd.paramDouble("HitCrossingOffset", 0.0);
if (phase == Phase::OUTPUT) {
const double cur = getInput(ctx, b.nid, 0, 0.0) - off;
double out = 0.0;
if (st.scalar2 != 0.0) {
out = hcHit(st.scalar, cur, hcDirection(bd)) ? 1.0 : 0.0;
} else if (cur == 0.0) {
// Starts on the offset: a hit, whatever direction was asked for
// (there is no previous sample to give the crossing a sign).
out = 1.0;
}
setOutput(ctx, b.nid, out);
return false;
}
if (phase == Phase::UPDATE) {
st.scalar = getInput(ctx, b.nid, 0, 0.0) - off;
st.scalar2 = 1.0;
return false;
}
return false;
}
//=============================================================================
static std::string
hcCondC(const BlockCodegenArgs& a, const std::string& c, int dir)
{
const std::string p = "s->hc_prev_" + a.id;
const std::string rising = "(" + p + " < 0.0 && " + c + " >= 0.0)";
const std::string falling = "(" + p + " > 0.0 && " + c + " <= 0.0)";
if (dir == 1) {
return rising;
}
if (dir == 2) {
return falling;
}
return "(" + rising + " || " + falling + ")";
}
//=============================================================================
BlockCodegenTemplate
getCodeGenCHitCrossing()
{
BlockCodegenTemplate t;
t.emitState = [](const BlockCodegenStateArgs& a) {
a.addState("hc_prev_" + a.id, "0.0", "");
a.addState("hc_primed_" + a.id, "0.0", "");
};
t.emitStep = [](const BlockCodegenArgs& a) {
nflow::BlockDescriptor bd(*a.block, *a.variables);
const std::string off = a.fmt(bd.paramDouble("HitCrossingOffset", 0.0));
const int dir = hcDirection(bd);
const std::string c = "(" + a.in[0] + " - " + off + ")";
// Before the first latch, a hit is the input STARTING on the offset.
a.line("out_" + a.id + " = (s->hc_primed_" + a.id + " != 0.0) ? ((" + hcCondC(a, c, dir)
+ ") ? 1.0 : 0.0) : ((" + c + " == 0.0) ? 1.0 : 0.0);");
a.line("s->hc_prev_" + a.id + " = " + c + ";");
a.line("s->hc_primed_" + a.id + " = 1.0;");
};
return t;
}
//=============================================================================
static std::string
hcCondRust(const BlockCodegenArgs& a, const std::string& c, int dir)
{
const std::string p = "s.hc_prev_" + a.id;
const std::string rising = "(" + p + " < 0.0_f64 && " + c + " >= 0.0_f64)";
const std::string falling = "(" + p + " > 0.0_f64 && " + c + " <= 0.0_f64)";
if (dir == 1) {
return rising;
}
if (dir == 2) {
return falling;
}
return "(" + rising + " || " + falling + ")";
}
//=============================================================================
BlockCodegenTemplate
getCodeGenRustHitCrossing()
{
BlockCodegenTemplate t;
t.emitState = [](const BlockCodegenStateArgs& a) {
a.addState("hc_prev_" + a.id, "0.0_f64", "");
a.addState("hc_primed_" + a.id, "0.0_f64", "");
};
t.emitStep = [](const BlockCodegenArgs& a) {
nflow::BlockDescriptor bd(*a.block, *a.variables);
const std::string off = a.fmt(bd.paramDouble("HitCrossingOffset", 0.0));
const int dir = hcDirection(bd);
const std::string c = "(" + a.in[0] + " - " + off + ")";
// Before the first latch, a hit is the input STARTING on the offset.
a.line("out_" + a.id + " = if s.hc_primed_" + a.id + " != 0.0_f64 { if "
+ hcCondRust(a, c, dir) + " { 1.0_f64 } else { 0.0_f64 } } else if " + c
+ " == 0.0_f64 { 1.0_f64 } else { 0.0_f64 };");
a.line("s.hc_prev_" + a.id + " = " + c + ";");
a.line("s.hc_primed_" + a.id + " = 1.0_f64;");
};
return t;
}
//=============================================================================
} // namespace NFlow
} // namespace Nelson
//=============================================================================