From 7b72ee64a798260a145f94370befebef4c1a0d94 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 11:36:31 +0100 Subject: [PATCH 1/3] Add new pathsim blocks: Divider, logic ops, Atan2, Rescale, Alias --- scripts/config/pathsim/blocks.json | 13 +++ src/lib/constants/python.ts | 1 + src/lib/nodes/generated/blocks.ts | 181 ++++++++++++++++++++++++++++- src/lib/nodes/shapes/registry.ts | 1 + src/lib/nodes/uiConfig.ts | 5 +- src/lib/types/nodes.ts | 1 + 6 files changed, 196 insertions(+), 6 deletions(-) diff --git a/scripts/config/pathsim/blocks.json b/scripts/config/pathsim/blocks.json index bcc5e34e..5367ef8d 100644 --- a/scripts/config/pathsim/blocks.json +++ b/scripts/config/pathsim/blocks.json @@ -46,6 +46,7 @@ "Algebraic": [ "Adder", "Multiplier", + "Divider", "Amplifier", "Function", "Sin", @@ -60,11 +61,23 @@ "Mod", "Clip", "Pow", + "Atan2", + "Rescale", + "Alias", "Switch", "LUT", "LUT1D" ], + "Logic": [ + "GreaterThan", + "LessThan", + "Equal", + "LogicAnd", + "LogicOr", + "LogicNot" + ], + "Mixed": [ "SampleHold", "FIR", diff --git a/src/lib/constants/python.ts b/src/lib/constants/python.ts index 29a28f0a..9ed9043a 100644 --- a/src/lib/constants/python.ts +++ b/src/lib/constants/python.ts @@ -28,6 +28,7 @@ export const BLOCK_CATEGORY_ORDER: string[] = [ 'Sources', 'Dynamic', 'Algebraic', + 'Logic', 'Mixed', 'Recording', 'Subsystem' diff --git a/src/lib/nodes/generated/blocks.ts b/src/lib/nodes/generated/blocks.ts index af8c6fe8..75e3af2e 100644 --- a/src/lib/nodes/generated/blocks.ts +++ b/src/lib/nodes/generated/blocks.ts @@ -405,12 +405,17 @@ export const extractedBlocks: Record = "Delay": { "blockClass": "Delay", "description": "Delays the input signal by a time constant 'tau' in seconds.", - "docstringHtml": "

Delays the input signal by a time constant 'tau' in seconds.

\n

Mathematically this block creates a time delay of the input signal like this:

\n
\n\\begin{equation*}\ny(t) =\n\\begin{cases}\nx(t - \\tau) & , t \\geq \\tau \\\\\n0 & , t < \\tau\n\\end{cases}\n\\end{equation*}\n
\n
\n

Note

\n

The internal adaptive buffer uses interpolation for the evaluation. This is\nrequired to be compatible with variable step solvers. It has a drawback however.\nThe order of the ode solver used will degrade when this block is used, due to\nthe interpolation.

\n
\n
\n

Note

\n

This block supports vector input, meaning we can have multiple parallel\ndelay paths through this block.

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#5 time units delay\nD = Delay(tau=5)\n
\n
\n
\n

Parameters

\n
\n
tau : float
\n
delay time constant
\n
\n
\n
\n

Attributes

\n
\n
_buffer : AdaptiveBuffer
\n
internal interpolatable adaptive rolling buffer
\n
\n
\n", + "docstringHtml": "

Delays the input signal by a time constant 'tau' in seconds.

\n

Supports two modes of operation:

\n

Continuous mode (default, sampling_period=None):\nUses an adaptive interpolating buffer for continuous-time delay.

\n
\n\\begin{equation*}\ny(t) =\n\\begin{cases}\nx(t - \\tau) & , t \\geq \\tau \\\\\n0 & , t < \\tau\n\\end{cases}\n\\end{equation*}\n
\n

Discrete mode (sampling_period provided):\nUses a ring buffer with scheduled sampling events for N-sample delay,\nwhere N = round(tau / sampling_period).

\n
\n\\begin{equation*}\ny[k] = x[k - N]\n\\end{equation*}\n
\n
\n

Note

\n

In continuous mode, the internal adaptive buffer uses interpolation for\nthe evaluation. This is required to be compatible with variable step solvers.\nIt has a drawback however. The order of the ode solver used will degrade\nwhen this block is used, due to the interpolation.

\n
\n
\n

Note

\n

This block supports vector input, meaning we can have multiple parallel\ndelay paths through this block.

\n
\n
\n

Example

\n

Continuous-time delay:

\n
\n#5 time units delay\nD = Delay(tau=5)\n
\n

Discrete-time N-sample delay (10 samples):

\n
\nD = Delay(tau=0.01, sampling_period=0.001)\n
\n
\n
\n

Parameters

\n
\n
tau : float
\n
delay time constant in seconds
\n
sampling_period : float, None
\n
sampling period for discrete mode, default is continuous mode
\n
\n
\n
\n

Attributes

\n
\n
_buffer : AdaptiveBuffer
\n
internal interpolatable adaptive rolling buffer (continuous mode)
\n
_ring : deque
\n
internal ring buffer for N-sample delay (discrete mode)
\n
\n
\n", "params": { "tau": { "type": "number", "default": "0.001", - "description": "delay time constant" + "description": "delay time constant in seconds" + }, + "sampling_period": { + "type": "any", + "default": null, + "description": "sampling period for discrete mode, default is continuous mode" } }, "inputs": null, @@ -896,6 +901,27 @@ export const extractedBlocks: Record = "out" ] }, + "Divider": { + "blockClass": "Divider", + "description": "Multiplies and divides input signals (MISO).", + "docstringHtml": "

Multiplies and divides input signals (MISO).

\n

This is the default behavior (multiply all):

\n
\n\\begin{equation*}\ny(t) = \\prod_i u_i(t)\n\\end{equation*}\n
\n

and this is the behavior with an operations string:

\n
\n\\begin{equation*}\ny(t) = \\frac{\\prod_{i \\in M} u_i(t)}{\\prod_{j \\in D} u_j(t)}\n\\end{equation*}\n
\n

where \\(M\\) is the set of inputs with * and \\(D\\) the set with /.

\n
\n

Example

\n

Default initialization multiplies the first input and divides by the second:

\n
\nD = Divider()\n
\n

Multiply the first two inputs and divide by the third:

\n
\nD = Divider('**/')\n
\n

Raise an error instead of producing inf when a denominator input is zero:

\n
\nD = Divider('**/', zero_div='raise')\n
\n

Clamp the denominator to machine epsilon so the output stays finite:

\n
\nD = Divider('**/', zero_div='clamp')\n
\n
\n
\n

Note

\n

This block is purely algebraic and its operation (op_alg) will be called\nmultiple times per timestep, each time when Simulation._update(t) is\ncalled in the global simulation loop.

\n
\n
\n

Parameters

\n
\n
operations : str, optional
\n
String of * and / characters indicating which inputs are\nmultiplied (*) or divided (/). Inputs beyond the length of\nthe string default to *. Defaults to '*/' (divide second\ninput by first).
\n
zero_div : str, optional
\n

Behaviour when a denominator input is zero. One of:

\n
\n
'warn' (default)
\n
Propagates inf and emits a RuntimeWarning — numpy's\nstandard behaviour.
\n
'raise'
\n
Raises ZeroDivisionError.
\n
'clamp'
\n
Clamps the denominator magnitude to machine epsilon\n(numpy.finfo(float).eps), preserving sign, so the output\nstays large-but-finite rather than inf.
\n
\n
\n
\n
\n
\n

Attributes

\n
\n
_ops : dict
\n
Maps operation characters to exponent values (+1 or -1).
\n
_ops_array : numpy.ndarray
\n
Exponents (+1 for *, -1 for /) converted to an array.
\n
op_alg : Operator
\n
Internal algebraic operator.
\n
\n
\n", + "params": { + "operations": { + "type": "string", + "default": "\"*/\"", + "description": "String of ``*`` and ``/`` characters indicating which inputs are multiplied (``*``) or divided (``/``). Inputs beyond the length of the string default to ``*``. Defaults to ``'*/'`` (divide second input by first)." + }, + "zero_div": { + "type": "string", + "default": "\"warn\"", + "description": "Behaviour when a denominator input is zero. One of:" + } + }, + "inputs": null, + "outputs": [ + "out" + ] + }, "Amplifier": { "blockClass": "Amplifier", "description": "Amplifies the input signal by multiplication with a constant gain term.", @@ -1043,12 +1069,67 @@ export const extractedBlocks: Record = "inputs": null, "outputs": null }, + "Atan2": { + "blockClass": "Atan2", + "description": "Two-argument arctangent block.", + "docstringHtml": "

Two-argument arctangent block.

\n

Computes the four-quadrant arctangent of two inputs:

\n
\n\\begin{equation*}\ny = \\mathrm{atan2}(a, b)\n\\end{equation*}\n
\n
\n

Note

\n

This block takes exactly two inputs (a, b) and produces one output.\nThe first input is the y-coordinate, the second is the x-coordinate,\nmatching the convention of numpy.arctan2(y, x).

\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "Rescale": { + "blockClass": "Rescale", + "description": "Linear rescaling / mapping block.", + "docstringHtml": "

Linear rescaling / mapping block.

\n

Maps the input linearly from range [i0, i1] to range [o0, o1].\nOptionally saturates the output to [o0, o1].

\n
\n\\begin{equation*}\ny = o_0 + \\frac{(x - i_0) \\cdot (o_1 - o_0)}{i_1 - i_0}\n\\end{equation*}\n
\n

This block supports vector inputs.

\n
\n

Parameters

\n
\n
i0 : float
\n
input range lower bound
\n
i1 : float
\n
input range upper bound
\n
o0 : float
\n
output range lower bound
\n
o1 : float
\n
output range upper bound
\n
saturate : bool
\n
if True, clamp output to [min(o0,o1), max(o0,o1)]
\n
\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": { + "i0": { + "type": "number", + "default": "0.0", + "description": "input range lower bound" + }, + "i1": { + "type": "number", + "default": "1.0", + "description": "input range upper bound" + }, + "o0": { + "type": "number", + "default": "0.0", + "description": "output range lower bound" + }, + "o1": { + "type": "number", + "default": "1.0", + "description": "output range upper bound" + }, + "saturate": { + "type": "boolean", + "default": "false", + "description": "if True, clamp output to [min(o0,o1), max(o0,o1)]" + } + }, + "inputs": null, + "outputs": null + }, + "Alias": { + "blockClass": "Alias", + "description": "Signal alias / pass-through block.", + "docstringHtml": "

Signal alias / pass-through block.

\n

Passes the input directly to the output without modification.\nThis is useful for signal renaming in model composition.

\n
\n\\begin{equation*}\ny = x\n\\end{equation*}\n
\n

This block supports vector inputs.

\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": null, + "outputs": null + }, "Switch": { "blockClass": "Switch", "description": "Switch block that selects between its inputs.", - "docstringHtml": "

Switch block that selects between its inputs.

\n
\n

Example

\n

The block is initialized like this:

\n
\n#default None -> no passthrough\ns1 = Switch()\n\n#selecting port 2 as passthrough\ns2 = Switch(2)\n\n#change the state of the switch to port 3\ns2.select(3)\n
\n

Sets block output depending on self.state like this:

\n
\nstate == None -> outputs[0] = 0\n\nstate == 0 -> outputs[0] = inputs[0]\n\nstate == 1 -> outputs[0] = inputs[1]\n\nstate == 2 -> outputs[0] = inputs[2]\n\n...\n
\n
\n
\n

Parameters

\n
\n
state : int, None
\n
state of the switch
\n
\n
\n", + "docstringHtml": "

Switch block that selects between its inputs.

\n
\n

Example

\n

The block is initialized like this:

\n
\n#default None -> no passthrough\ns1 = Switch()\n\n#selecting port 2 as passthrough\ns2 = Switch(2)\n\n#change the state of the switch to port 3\ns2.select(3)\n
\n

Sets block output depending on self.switch_state like this:

\n
\nswitch_state == None -> outputs[0] = 0\n\nswitch_state == 0 -> outputs[0] = inputs[0]\n\nswitch_state == 1 -> outputs[0] = inputs[1]\n\nswitch_state == 2 -> outputs[0] = inputs[2]\n\n...\n
\n
\n
\n

Parameters

\n
\n
switch_state : int, None
\n
state of the switch
\n
\n
\n", "params": { - "state": { + "switch_state": { "type": "any", "default": null, "description": "state of the switch" @@ -1102,6 +1183,85 @@ export const extractedBlocks: Record = "inputs": null, "outputs": null }, + "GreaterThan": { + "blockClass": "GreaterThan", + "description": "Greater-than comparison block.", + "docstringHtml": "

Greater-than comparison block.

\n

Compares two inputs and outputs 1.0 if a > b, else 0.0.

\n
\n\\begin{equation*}\ny =\n\\begin{cases}\n1 & , a > b \\\\\n0 & , a \\leq b\n\\end{cases}\n\\end{equation*}\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "LessThan": { + "blockClass": "LessThan", + "description": "Less-than comparison block.", + "docstringHtml": "

Less-than comparison block.

\n

Compares two inputs and outputs 1.0 if a < b, else 0.0.

\n
\n\\begin{equation*}\ny =\n\\begin{cases}\n1 & , a < b \\\\\n0 & , a \\geq b\n\\end{cases}\n\\end{equation*}\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "Equal": { + "blockClass": "Equal", + "description": "Equality comparison block.", + "docstringHtml": "

Equality comparison block.

\n

Compares two inputs and outputs 1.0 if |a - b| <= tolerance, else 0.0.

\n
\n\\begin{equation*}\ny =\n\\begin{cases}\n1 & , |a - b| \\leq \\epsilon \\\\\n0 & , |a - b| > \\epsilon\n\\end{cases}\n\\end{equation*}\n
\n
\n

Parameters

\n
\n
tolerance : float
\n
comparison tolerance for floating point equality
\n
\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": { + "tolerance": { + "type": "number", + "default": "1e-12", + "description": "comparison tolerance for floating point equality" + } + }, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "LogicAnd": { + "blockClass": "LogicAnd", + "description": "Logical AND block.", + "docstringHtml": "

Logical AND block.

\n

Outputs 1.0 if both inputs are nonzero, else 0.0.

\n
\n\\begin{equation*}\ny = a \\land b\n\\end{equation*}\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "LogicOr": { + "blockClass": "LogicOr", + "description": "Logical OR block.", + "docstringHtml": "

Logical OR block.

\n

Outputs 1.0 if either input is nonzero, else 0.0.

\n
\n\\begin{equation*}\ny = a \\lor b\n\\end{equation*}\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": [ + "a", + "b" + ], + "outputs": [ + "y" + ] + }, + "LogicNot": { + "blockClass": "LogicNot", + "description": "Logical NOT block.", + "docstringHtml": "

Logical NOT block.

\n

Outputs 1.0 if input is zero, else 0.0.

\n
\n\\begin{equation*}\ny = \\lnot x\n\\end{equation*}\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator
\n
\n
\n", + "params": {}, + "inputs": null, + "outputs": null + }, "SampleHold": { "blockClass": "SampleHold", "description": "Samples the inputs periodically and produces them at the output.", @@ -1521,7 +1681,8 @@ export const extractedBlocks: Record = export const blockConfig: Record = { Sources: ["Constant", "Source", "SinusoidalSource", "StepSource", "PulseSource", "TriangleWaveSource", "SquareWaveSource", "GaussianPulseSource", "ChirpPhaseNoiseSource", "ClockSource", "WhiteNoise", "PinkNoise", "RandomNumberGenerator"], Dynamic: ["Integrator", "Differentiator", "Delay", "ODE", "DynamicalSystem", "StateSpace", "PT1", "PT2", "LeadLag", "PID", "AntiWindupPID", "RateLimiter", "Backlash", "Deadband", "TransferFunctionNumDen", "TransferFunctionZPG", "ButterworthLowpassFilter", "ButterworthHighpassFilter", "ButterworthBandpassFilter", "ButterworthBandstopFilter"], - Algebraic: ["Adder", "Multiplier", "Amplifier", "Function", "Sin", "Cos", "Tan", "Tanh", "Abs", "Sqrt", "Exp", "Log", "Log10", "Mod", "Clip", "Pow", "Switch", "LUT", "LUT1D"], + Algebraic: ["Adder", "Multiplier", "Divider", "Amplifier", "Function", "Sin", "Cos", "Tan", "Tanh", "Abs", "Sqrt", "Exp", "Log", "Log10", "Mod", "Clip", "Pow", "Atan2", "Rescale", "Alias", "Switch", "LUT", "LUT1D"], + Logic: ["GreaterThan", "LessThan", "Equal", "LogicAnd", "LogicOr", "LogicNot"], Mixed: ["SampleHold", "FIR", "ADC", "DAC", "Counter", "CounterUp", "CounterDown", "Relay"], Recording: ["Scope", "Spectrum"], Chemical: ["Process", "Bubbler4", "Splitter", "GLC"], @@ -1531,8 +1692,10 @@ export const blockImportPaths: Record = { "ADC": "pathsim.blocks", "Abs": "pathsim.blocks", "Adder": "pathsim.blocks", + "Alias": "pathsim.blocks", "Amplifier": "pathsim.blocks", "AntiWindupPID": "pathsim.blocks", + "Atan2": "pathsim.blocks", "Backlash": "pathsim.blocks", "Bubbler4": "pathsim_chem.tritium", "ButterworthBandpassFilter": "pathsim.blocks", @@ -1551,18 +1714,25 @@ export const blockImportPaths: Record = { "Deadband": "pathsim.blocks", "Delay": "pathsim.blocks", "Differentiator": "pathsim.blocks", + "Divider": "pathsim.blocks", "DynamicalSystem": "pathsim.blocks", + "Equal": "pathsim.blocks", "Exp": "pathsim.blocks", "FIR": "pathsim.blocks", "Function": "pathsim.blocks", "GLC": "pathsim_chem.tritium", "GaussianPulseSource": "pathsim.blocks", + "GreaterThan": "pathsim.blocks", "Integrator": "pathsim.blocks", "LUT": "pathsim.blocks", "LUT1D": "pathsim.blocks", "LeadLag": "pathsim.blocks", + "LessThan": "pathsim.blocks", "Log": "pathsim.blocks", "Log10": "pathsim.blocks", + "LogicAnd": "pathsim.blocks", + "LogicNot": "pathsim.blocks", + "LogicOr": "pathsim.blocks", "Mod": "pathsim.blocks", "Multiplier": "pathsim.blocks", "ODE": "pathsim.blocks", @@ -1576,6 +1746,7 @@ export const blockImportPaths: Record = { "RandomNumberGenerator": "pathsim.blocks", "RateLimiter": "pathsim.blocks", "Relay": "pathsim.blocks", + "Rescale": "pathsim.blocks", "SampleHold": "pathsim.blocks", "Scope": "pathsim.blocks", "Sin": "pathsim.blocks", diff --git a/src/lib/nodes/shapes/registry.ts b/src/lib/nodes/shapes/registry.ts index e000d82e..cf98e1a6 100644 --- a/src/lib/nodes/shapes/registry.ts +++ b/src/lib/nodes/shapes/registry.ts @@ -87,6 +87,7 @@ const categoryShapeMap: Record = { Sources: 'pill', Dynamic: 'rect', Algebraic: 'rect', + Logic: 'diamond', Mixed: 'mixed', Recording: 'pill', Subsystem: 'rect' diff --git a/src/lib/nodes/uiConfig.ts b/src/lib/nodes/uiConfig.ts index b5ca3337..e611ce9c 100644 --- a/src/lib/nodes/uiConfig.ts +++ b/src/lib/nodes/uiConfig.ts @@ -41,7 +41,8 @@ function parseOperationsString(value: unknown): string[] | null { export const portLabelParams: Record = { Scope: { param: 'labels', direction: 'input' }, Spectrum: { param: 'labels', direction: 'input' }, - Adder: { param: 'operations', direction: 'input', parser: parseOperationsString } + Adder: { param: 'operations', direction: 'input', parser: parseOperationsString }, + Divider: { param: 'operations', direction: 'input', parser: parseOperationsString } }; /** @@ -77,6 +78,8 @@ export const syncPortBlocks = new Set([ 'Mod', 'Clip', 'Pow', + 'Rescale', + 'Alias', // Mixed blocks (parallel sampling) 'SampleHold' diff --git a/src/lib/types/nodes.ts b/src/lib/types/nodes.ts index c1583847..0d247201 100644 --- a/src/lib/types/nodes.ts +++ b/src/lib/types/nodes.ts @@ -44,6 +44,7 @@ export type NodeCategory = | 'Sources' | 'Dynamic' | 'Algebraic' + | 'Logic' | 'Mixed' | 'Recording' | 'Subsystem'; From 3dd82145df132b7d850beeb3cc80a803478489d1 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 11:52:42 +0100 Subject: [PATCH 2/3] Use rect shape for Logic category instead of octagon --- src/lib/nodes/shapes/registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/nodes/shapes/registry.ts b/src/lib/nodes/shapes/registry.ts index cf98e1a6..a46ded2e 100644 --- a/src/lib/nodes/shapes/registry.ts +++ b/src/lib/nodes/shapes/registry.ts @@ -87,7 +87,7 @@ const categoryShapeMap: Record = { Sources: 'pill', Dynamic: 'rect', Algebraic: 'rect', - Logic: 'diamond', + Logic: 'rect', Mixed: 'mixed', Recording: 'pill', Subsystem: 'rect' From f99457722ddc516968f916d28a5d39006c94644a Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 17:22:38 +0100 Subject: [PATCH 3/3] Add LogicNot to syncPortBlocks for synced I/O ports --- src/lib/nodes/uiConfig.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/nodes/uiConfig.ts b/src/lib/nodes/uiConfig.ts index e611ce9c..ca3a89b6 100644 --- a/src/lib/nodes/uiConfig.ts +++ b/src/lib/nodes/uiConfig.ts @@ -81,6 +81,9 @@ export const syncPortBlocks = new Set([ 'Rescale', 'Alias', + // Logic blocks (element-wise) + 'LogicNot', + // Mixed blocks (parallel sampling) 'SampleHold' ]);