From 77d941164c9ad611e9d5af5bcbe1f20898faaba7 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 16 Feb 2026 13:59:12 +0100 Subject: [PATCH 1/2] Add PT1, PT2, LeadLag, RateLimiter blocks and update PID to SISO --- scripts/config/pathsim/blocks.json | 4 + scripts/generated/registry.json | 34 +++++ src/lib/constants/dependencies.ts | 8 +- src/lib/nodes/generated/blocks.ts | 198 ++++++++++++++++++++++++----- src/lib/nodes/uiConfig.ts | 3 - 5 files changed, 211 insertions(+), 36 deletions(-) diff --git a/scripts/config/pathsim/blocks.json b/scripts/config/pathsim/blocks.json index 0c0a85bc..4163e87e 100644 --- a/scripts/config/pathsim/blocks.json +++ b/scripts/config/pathsim/blocks.json @@ -27,8 +27,12 @@ "ODE", "DynamicalSystem", "StateSpace", + "PT1", + "PT2", + "LeadLag", "PID", "AntiWindupPID", + "RateLimiter", "TransferFunctionNumDen", "TransferFunctionZPG", "ButterworthLowpassFilter", diff --git a/scripts/generated/registry.json b/scripts/generated/registry.json index 6e9b566c..67027734 100644 --- a/scripts/generated/registry.json +++ b/scripts/generated/registry.json @@ -171,6 +171,32 @@ "initial_value" ] }, + "PT1": { + "blockClass": "PT1", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T" + ] + }, + "PT2": { + "blockClass": "PT2", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T", + "d" + ] + }, + "LeadLag": { + "blockClass": "LeadLag", + "importPath": "pathsim.blocks", + "params": [ + "K", + "T1", + "T2" + ] + }, "PID": { "blockClass": "PID", "importPath": "pathsim.blocks", @@ -193,6 +219,14 @@ "limits" ] }, + "RateLimiter": { + "blockClass": "RateLimiter", + "importPath": "pathsim.blocks", + "params": [ + "rate", + "f_max" + ] + }, "TransferFunctionNumDen": { "blockClass": "TransferFunctionNumDen", "importPath": "pathsim.blocks", diff --git a/src/lib/constants/dependencies.ts b/src/lib/constants/dependencies.ts index 9f1655ac..7a0e1fbb 100644 --- a/src/lib/constants/dependencies.ts +++ b/src/lib/constants/dependencies.ts @@ -1,7 +1,7 @@ // Auto-generated by scripts/extract.py - DO NOT EDIT // Source: scripts/config/requirements-pyodide.txt, scripts/config/pyodide.json -export const PATHVIEW_VERSION = '0.4.5'; +export const PATHVIEW_VERSION = '0.6.1'; export const PYODIDE_VERSION = '0.26.2'; export const PYODIDE_CDN_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.mjs`; @@ -9,7 +9,7 @@ export const PYODIDE_CDN_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERS export const PYODIDE_PRELOAD = ["numpy", "scipy", "micropip"] as const; /** Package versions extracted at build time (pinned for runtime) */ -export const EXTRACTED_VERSIONS: Record = {"pathsim": "0.16.5", "pathsim_chem": "0.2rc3.dev1"}; +export const EXTRACTED_VERSIONS: Record = {"pathsim": "0.16.8.dev3", "pathsim_chem": "0.2rc3"}; export interface PackageConfig { pip: string; @@ -21,13 +21,13 @@ export interface PackageConfig { export const PYTHON_PACKAGES: PackageConfig[] = [ { - "pip": "pathsim==0.16.5", + "pip": "pathsim", "required": true, "pre": true, "import": "pathsim" }, { - "pip": "pathsim-chem>=0.2rc2", + "pip": "pathsim-chem==0.2rc3", "required": false, "pre": true, "import": "pathsim_chem" diff --git a/src/lib/nodes/generated/blocks.ts b/src/lib/nodes/generated/blocks.ts index 2bbc5539..dfd5d184 100644 --- a/src/lib/nodes/generated/blocks.ts +++ b/src/lib/nodes/generated/blocks.ts @@ -40,7 +40,7 @@ export const extractedBlocks: Record = "Source": { "blockClass": "Source", "description": "Source that produces an arbitrary time dependent output defined by `func` (callable).", - "docstringHtml": "

Source that produces an arbitrary time dependent output defined by func (callable).

\n
\n\\begin{equation*}\ny(t) = \\mathrm{func}(t)\n\\end{equation*}\n
\n
\n

Note

\n

This block is purely algebraic and its internal function (func) will\nbe called multiple times per timestep, each time when Simulation._update(t)\nis called in the global simulation loop.

\n
\n
\n

Example

\n

For example a ramp:

\n
\nfrom pathsim.blocks import Source\n\nsrc = Source(lambda t : t)\n
\n

or a simple sinusoid with some frequency:

\n
\nimport numpy as np\nfrom pathsim.blocks import Source\n\n#some parameter\nomega = 100\n\n#the function that gets evaluated\ndef f(t):\n    return np.sin(omega * t)\n\nsrc = Source(f)\n
\n

Because the Source block only has a single argument, it can be\nused to decorate a function and make it a PathSim block. This might\nbe handy in some cases to keep definitions concise and localized\nin the code:

\n
\nimport numpy as np\nfrom pathsim.blocks import Source\n\n#does the same as the definition above\n\n@Source\ndef src(t):\n    omega = 100\n    return np.sin(omega * t)\n\n#'src' is now a PathSim block\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
function defining time dependent block output
\n
\n
\n", + "docstringHtml": "

Source that produces an arbitrary time dependent output defined by func (callable).

\n
\n\\begin{equation*}\ny(t) = \\mathrm{func}(t)\n\\end{equation*}\n
\n
\n

Note

\n

This block is purely algebraic and its internal function (func) will\nbe called multiple times per timestep, each time when Simulation._update(t)\nis called in the global simulation loop.

\n
\n
\n

Example

\n

For example a ramp:

\n
\nfrom pathsim.blocks import Source\n\nsrc = Source(lambda t : t)\n
\n

or a simple sinusoid with some frequency:

\n
\nimport numpy as np\nfrom pathsim.blocks import Source\n\n#some parameter\nomega = 100\n\n#the function that gets evaluated\ndef f(t):\n    return np.sin(omega * t)\n\nsrc = Source(f)\n
\n

Because the Source block only has a single argument, it can be\nused to decorate a function and make it a PathSim block. This might\nbe handy in some cases to keep definitions concise and localized\nin the code:

\n
\nimport numpy as np\nfrom pathsim.blocks import Source\n\n#does the same as the definition above\n\n@Source\ndef src(t):\n    omega = 100\n    return np.sin(omega * t)\n\n#'src' is now a PathSim block\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
function defining time dependent block output
\n
\n
\n", "params": { "func": { "type": "callable", @@ -82,7 +82,7 @@ export const extractedBlocks: Record = "StepSource": { "blockClass": "StepSource", "description": "Discrete time unit step (or multi step) source block.", - "docstringHtml": "

Discrete time unit step (or multi step) source block.

\n

Utilizes a scheduled event to set the block output\nto the specified output levels at the defined event times.

\n

The arguments can be vectorial and in that case, the output is set to the\namplitude that corresponds to the defined delay like a zero-order-hold stage.\nThis functionality enables adding external or time series measurement data\ninto the system.

\n
\n

Examples

\n

This is how to use the source as a unit step source:

\n
\nfrom pathsim.blocks import StepSource\n\n#default, starts at 0, jumps to 1\nstp = StepSource()\n
\n

And this is how to configure it with multiple consecutive steps:

\n
\nfrom pathsim.blocks import StepSource\n\n#starts at 0, jumps to 1 at 1, jumps to -1 at 2 and jumps back to 0 at 3\nstp = StepSource(amplitude=[1, -1, 0], tau=[1, 2, 3])\n
\n

Similarly implementing measured time series data via zoh:

\n
\nimport numpy as np\nfrom pathsim.blocks import StepSource\n\n#some random time series arrays\ntimes, data = np.linspace(0, 100, 1000), np.random.rand(1000)\n\n#pass them to the block\nstp = StepSource(amplitude=data, tau=times)\n
\n
\n
\n

Parameters

\n
\n
amplitude : float | list[float]
\n
amplitude of the step signal, or amplitudes / output\nlevels of the multiple steps
\n
tau : float | list[float]
\n
delay of the step, or delays of the different steps
\n
\n
\n
\n

Attributes

\n
\n
Evt : ScheduleList
\n
internal scheduled event directly accessible
\n
events : list[ScheduleList]
\n
list of interna events
\n
\n
\n", + "docstringHtml": "

Discrete time unit step (or multi step) source block.

\n

Utilizes a scheduled event to set the block output\nto the specified output levels at the defined event times.

\n

The arguments can be vectorial and in that case, the output is set to the\namplitude that corresponds to the defined delay like a zero-order-hold stage.\nThis functionality enables adding external or time series measurement data\ninto the system.

\n
\n

Examples

\n

This is how to use the source as a unit step source:

\n
\nfrom pathsim.blocks import StepSource\n\n#default, starts at 0, jumps to 1\nstp = StepSource()\n
\n

And this is how to configure it with multiple consecutive steps:

\n
\nfrom pathsim.blocks import StepSource\n\n#starts at 0, jumps to 1 at 1, jumps to -1 at 2 and jumps back to 0 at 3\nstp = StepSource(amplitude=[1, -1, 0], tau=[1, 2, 3])\n
\n

Similarly implementing measured time series data via zoh:

\n
\nimport numpy as np\nfrom pathsim.blocks import StepSource\n\n#some random time series arrays\ntimes, data = np.linspace(0, 100, 1000), np.random.rand(1000)\n\n#pass them to the block\nstp = StepSource(amplitude=data, tau=times)\n
\n
\n
\n

Parameters

\n
\n
amplitude : float | list[float]
\n
amplitude of the step signal, or amplitudes / output\nlevels of the multiple steps
\n
tau : float | list[float]
\n
delay of the step, or delays of the different steps
\n
\n
\n
\n

Attributes

\n
\n
Evt : ScheduleList
\n
internal scheduled event directly accessible
\n
events : list[ScheduleList]
\n
list of interna events
\n
\n
\n", "params": { "amplitude": { "type": "integer", @@ -419,7 +419,7 @@ export const extractedBlocks: Record = "ODE": { "blockClass": "ODE", "description": "Ordinary differential equation (ODE) defined by its right hand side function.", - "docstringHtml": "

Ordinary differential equation (ODE) defined by its right hand side function.

\n
\n\\begin{equation*}\n\\begin{align}\n \\dot{x}(t) &= \\mathrm{func}(x(t), u(t), t) \\\\\n y(t) &= x(t)\n\\end{align}\n\\end{equation*}\n
\n

with inhomogenity (input) u and state vector x. The function can be nonlinear\nand the ODE can be of arbitrary order. The block utilizes the integration engine\nto solve the ODE by integrating the func, which is the right hand side function.

\n
\n

Example

\n

For example a linear 1st order ODE:

\n
\node = ODE(lambda x, u, t: -x)\n
\n

Or something more complex like the Van der Pol system, where it makes sense to\nalso specify the jacobian, which improves convergence for implicit solvers but is\nnot needed in most cases:

\n
\nimport numpy as np\n\n#initial condition\nx0 = np.array([2, 0])\n\n#van der Pol parameter\nmu = 1000\n\ndef func(x, u, t):\n    return np.array([x[1], mu*(1 - x[0]**2)*x[1] - x[0]])\n\n#analytical jacobian (optional)\ndef jac(x, u, t):\n    return np.array(\n        [[0                , 1               ],\n         [-mu*2*x[0]*x[1]-1, mu*(1 - x[0]**2)]]\n         )\n\n#finally the block\nvdp = ODE(func, x0, jac)\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
right hand side function of ODE
\n
initial_value : array[float]
\n
initial state / initial condition
\n
jac : callable, None
\n
jacobian of 'func' or 'None'
\n
\n
\n
\n

Attributes

\n
\n
op_dyn : DynamicOperator
\n
internal dynamic operator for ODE right hand side 'func'
\n
\n
\n", + "docstringHtml": "

Ordinary differential equation (ODE) defined by its right hand side function.

\n
\n\\begin{equation*}\n\\begin{align}\n \\dot{x}(t) &= \\mathrm{func}(x(t), u(t), t) \\\\\n y(t) &= x(t)\n\\end{align}\n\\end{equation*}\n
\n

with inhomogenity (input) u and state vector x. The function can be nonlinear\nand the ODE can be of arbitrary order. The block utilizes the integration engine\nto solve the ODE by integrating the func, which is the right hand side function.

\n
\n

Example

\n

For example a linear 1st order ODE:

\n
\node = ODE(lambda x, u, t: -x)\n
\n

Or something more complex like the Van der Pol system, where it makes sense to\nalso specify the jacobian, which improves convergence for implicit solvers but is\nnot needed in most cases:

\n
\nimport numpy as np\n\n#initial condition\nx0 = np.array([2, 0])\n\n#van der Pol parameter\nmu = 1000\n\ndef func(x, u, t):\n    return np.array([x[1], mu*(1 - x[0]**2)*x[1] - x[0]])\n\n#analytical jacobian (optional)\ndef jac(x, u, t):\n    return np.array(\n        [[0                , 1               ],\n         [-mu*2*x[0]*x[1]-1, mu*(1 - x[0]**2)]]\n         )\n\n#finally the block\nvdp = ODE(func, x0, jac)\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
right hand side function of ODE
\n
initial_value : array[float]
\n
initial state / initial condition
\n
jac : callable, None
\n
jacobian of 'func' or 'None'
\n
\n
\n
\n

Attributes

\n
\n
op_dyn : DynamicOperator
\n
internal dynamic operator for ODE right hand side 'func'
\n
\n
\n", "params": { "func": { "type": "callable", @@ -503,15 +503,94 @@ export const extractedBlocks: Record = "inputs": null, "outputs": null }, + "PT1": { + "blockClass": "PT1", + "description": "First-order lag element (PT1).", + "docstringHtml": "

First-order lag element (PT1).

\n

The transfer function is defined as

\n
\n\\begin{equation*}\nH(s) = \\frac{K}{1 + T s}\n\\end{equation*}\n
\n

where K is the static gain and T is the time constant.

\n
\n

Example

\n

The block is initialized like this:

\n
\npt1 = PT1(K=2.0, T=0.5)\n
\n
\n
\n

Parameters

\n
\n
K : float
\n
static gain
\n
T : float
\n
time constant in seconds (must be > 0)
\n
\n
\n", + "params": { + "K": { + "type": "number", + "default": "1.0", + "description": "static gain" + }, + "T": { + "type": "number", + "default": "1.0", + "description": "time constant in seconds (must be > 0)" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, + "PT2": { + "blockClass": "PT2", + "description": "Second-order lag element (PT2).", + "docstringHtml": "

Second-order lag element (PT2).

\n

The transfer function is defined as

\n
\n\\begin{equation*}\nH(s) = \\frac{K}{1 + 2 d T s + T^2 s^2}\n\\end{equation*}\n
\n

where K is the static gain, T is the time constant\n(related to the natural frequency by \\(\\omega_n = 1/T\\))\nand d is the damping ratio.

\n

The damping ratio d controls the transient behavior:

\n
    \n
  • \\(d < 1\\): underdamped (oscillatory)
  • \n
  • \\(d = 1\\): critically damped
  • \n
  • \\(d > 1\\): overdamped
  • \n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#underdamped second-order system\npt2 = PT2(K=1.0, T=0.1, d=0.3)\n
\n
\n
\n

Parameters

\n
\n
K : float
\n
static gain
\n
T : float
\n
time constant in seconds (must be > 0)
\n
d : float
\n
damping ratio (must be >= 0)
\n
\n
\n", + "params": { + "K": { + "type": "number", + "default": "1.0", + "description": "static gain" + }, + "T": { + "type": "number", + "default": "1.0", + "description": "time constant in seconds (must be > 0)" + }, + "d": { + "type": "number", + "default": "1.0", + "description": "damping ratio (must be >= 0)" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, + "LeadLag": { + "blockClass": "LeadLag", + "description": "Lead-Lag compensator.", + "docstringHtml": "

Lead-Lag compensator.

\n

The transfer function is defined as

\n
\n\\begin{equation*}\nH(s) = K \\frac{T_1 s + 1}{T_2 s + 1}\n\\end{equation*}\n
\n

where K is the static gain, T1 is the lead time constant\nand T2 is the lag time constant.

\n
    \n
  • \\(T_1 > T_2\\): lead compensator (phase advance)
  • \n
  • \\(T_1 < T_2\\): lag compensator (phase lag)
  • \n
  • \\(T_1 = T_2\\): pure gain
  • \n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#lead compensator\nll = LeadLag(K=1.0, T1=0.5, T2=0.1)\n
\n
\n
\n

Parameters

\n
\n
K : float
\n
static gain
\n
T1 : float
\n
lead (numerator) time constant in seconds
\n
T2 : float
\n
lag (denominator) time constant in seconds (must be > 0)
\n
\n
\n", + "params": { + "K": { + "type": "number", + "default": "1.0", + "description": "static gain" + }, + "T1": { + "type": "number", + "default": "1.0", + "description": "lead (numerator) time constant in seconds" + }, + "T2": { + "type": "number", + "default": "1.0", + "description": "lag (denominator) time constant in seconds (must be > 0)" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, "PID": { "blockClass": "PID", - "description": "Proportional-Integral-Differntiation (PID) controller.", - "docstringHtml": "

Proportional-Integral-Differntiation (PID) controller.

\n

The transfer function is defined as

\n
\n\\begin{equation*}\nH_\\mathrm{diff}(s) = K_p + K_i \\frac{1}{s} + K_d \\frac{s}{1 + s / f_\\mathrm{max}}\n\\end{equation*}\n
\n

where the differentiation is approximated by a high pass filter that holds\nfor signals up to a frequency of approximately f_max.

\n
\n

Note

\n

Depending on f_max, the resulting system might become stiff or ill conditioned!\nAs a practical choice set f_max to 3x the highest expected signal frequency.\nSince this block uses an approximation of real differentiation, the approximation will\nnot hold if there are high frequency components present in the signal. For example if\nyou have discontinuities such as steps or square waves.

\n
\n
\n

Note

\n

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

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#cutoff at 1kHz\npid = PID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3)\n
\n
\n
\n

Parameters

\n
\n
Kp : float
\n
poroportional controller coefficient
\n
Ki : float
\n
integral controller coefficient
\n
Kd : float
\n
differentiator controller coefficient
\n
f_max : float
\n
highest expected signal frequency
\n
\n
\n
\n

Attributes

\n
\n
op_dyn : DynamicOperator
\n
internal dynamic operator for ODE component
\n
op_alg : DynamicOperator
\n
internal algebraic operator
\n
\n
\n", + "description": "Proportional-Integral-Differentiation (PID) controller.", + "docstringHtml": "

Proportional-Integral-Differentiation (PID) controller.

\n

The transfer function is defined as

\n
\n\\begin{equation*}\nH(s) = K_p + K_i \\frac{1}{s} + K_d \\frac{s}{1 + s / f_\\mathrm{max}}\n\\end{equation*}\n
\n

where the differentiation is approximated by a high pass filter that holds\nfor signals up to a frequency of approximately f_max.

\n

Internally realized as a linear state space model with two states\n(differentiator filter state and integrator state).

\n
\n

Note

\n

Depending on f_max, the resulting system might become stiff or ill conditioned!\nAs a practical choice set f_max to 3x the highest expected signal frequency.\nSince this block uses an approximation of real differentiation, the approximation will\nnot hold if there are high frequency components present in the signal. For example if\nyou have discontinuities such as steps or square waves.

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#cutoff at 1kHz\npid = PID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3)\n
\n
\n
\n

Parameters

\n
\n
Kp : float
\n
proportional controller coefficient
\n
Ki : float
\n
integral controller coefficient
\n
Kd : float
\n
differentiator controller coefficient
\n
f_max : float
\n
highest expected signal frequency
\n
\n
\n", "params": { "Kp": { "type": "integer", "default": "0", - "description": "poroportional controller coefficient" + "description": "proportional controller coefficient" }, "Ki": { "type": "integer", @@ -529,18 +608,22 @@ export const extractedBlocks: Record = "description": "highest expected signal frequency" } }, - "inputs": null, - "outputs": null + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] }, "AntiWindupPID": { "blockClass": "AntiWindupPID", - "description": "Proportional-Integral-Differntiation (PID) controller with anti-windup mechanism (back-calculation).", - "docstringHtml": "

Proportional-Integral-Differntiation (PID) controller with anti-windup mechanism (back-calculation).

\n

Anti-windup mechanisms are needed when the magnitude of the control signal\nfrom the PID controller is limited by some real world saturation. In these cases,\nthe integrator will continue to acumulate the control error and "wind itself up".\nOnce the setpoint is reached, this can result in significant overshoots. This\nimplementation adds a conditional feedback term to the internal integrator that\n"unwinds" it when the PID output crosses some limits. This is pretty much a\ndeadzone feedback element for the integrator.

\n

Mathematically, this block implements the following set of ODEs

\n
\n\\begin{equation*}\n\\begin{align}\n\\dot{x}_1 &= f_\\mathrm{max} (u - x_1) \\\\\n\\dot{x}_2 &= u - w\n\\end{align}\n\\end{equation*}\n
\n

with the anti-windup feedback (depending on the pid output)

\n
\n\\begin{equation*}\nw = K_s (y - \\min(\\max(y, y_\\mathrm{min}), y_\\mathrm{max}))\n\\end{equation*}\n
\n

and the output itself

\n
\n\\begin{equation*}\ny = K_p u - K_d f_\\mathrm{max} x_1 + K_i x_2\n\\end{equation*}\n
\n
\n

Note

\n

Depending on f_max, the resulting system might become stiff or ill conditioned!\nAs a practical choice set f_max to 3x the highest expected signal frequency.\nSince this block uses an approximation of real differentiation, the approximation will\nnot hold if there are high frequency components present in the signal. For example if\nyou have discontinuities such as steps or squere waves.

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#cutoff at 1kHz, windup limits at [-5, 5]\npid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, limits=[-5, 5])\n
\n
\n
\n

Parameters

\n
\n
Kp : float
\n
poroportional controller coefficient
\n
Ki : float
\n
integral controller coefficient
\n
Kd : float
\n
differentiator controller coefficient
\n
f_max : float
\n
highest expected signal frequency
\n
Ks : float
\n
feedback term for back calculation for anti-windup control of integrator
\n
limits : array_like[float]
\n
lower and upper limit for PID output that triggers anti-windup of integrator
\n
\n
\n
\n

Attributes

\n
\n
op_dyn : DynamicOperator
\n
internal dynamic operator for ODE component
\n
op_alg : DynamicOperator
\n
internal algebraic operator
\n
\n
\n", + "description": "Proportional-Integral-Differentiation (PID) controller with anti-windup mechanism (back-calculation).", + "docstringHtml": "

Proportional-Integral-Differentiation (PID) controller with anti-windup mechanism (back-calculation).

\n

Anti-windup mechanisms are needed when the magnitude of the control signal\nfrom the PID controller is limited by some real world saturation. In these cases,\nthe integrator will continue to accumulate the control error and "wind itself up".\nOnce the setpoint is reached, this can result in significant overshoots. This\nimplementation adds a conditional feedback term to the internal integrator that\n"unwinds" it when the PID output crosses some limits. This is pretty much a\ndeadzone feedback element for the integrator.

\n

Mathematically, this block implements the following set of ODEs

\n
\n\\begin{equation*}\n\\begin{align}\n\\dot{x}_1 &= f_\\mathrm{max} (u - x_1) \\\\\n\\dot{x}_2 &= u - w\n\\end{align}\n\\end{equation*}\n
\n

with the anti-windup feedback (depending on the pid output)

\n
\n\\begin{equation*}\nw = K_s (y - \\min(\\max(y, y_\\mathrm{min}), y_\\mathrm{max}))\n\\end{equation*}\n
\n

and the output itself

\n
\n\\begin{equation*}\ny = K_p u + K_d f_\\mathrm{max} (u - x_1) + K_i x_2\n\\end{equation*}\n
\n
\n

Note

\n

Depending on f_max, the resulting system might become stiff or ill conditioned!\nAs a practical choice set f_max to 3x the highest expected signal frequency.\nSince this block uses an approximation of real differentiation, the approximation will\nnot hold if there are high frequency components present in the signal. For example if\nyou have discontinuities such as steps or square waves.

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#cutoff at 1kHz, windup limits at [-5, 5]\npid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, limits=[-5, 5])\n
\n
\n
\n

Parameters

\n
\n
Kp : float
\n
proportional controller coefficient
\n
Ki : float
\n
integral controller coefficient
\n
Kd : float
\n
differentiator controller coefficient
\n
f_max : float
\n
highest expected signal frequency
\n
Ks : float
\n
feedback term for back calculation for anti-windup control of integrator
\n
limits : array_like[float]
\n
lower and upper limit for PID output that triggers anti-windup of integrator
\n
\n
\n", "params": { "Kp": { "type": "integer", "default": "0", - "description": "poroportional controller coefficient" + "description": "proportional controller coefficient" }, "Ki": { "type": "integer", @@ -568,8 +651,35 @@ export const extractedBlocks: Record = "description": "lower and upper limit for PID output that triggers anti-windup of integrator" } }, - "inputs": null, - "outputs": null + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, + "RateLimiter": { + "blockClass": "RateLimiter", + "description": "Rate limiter block that limits the rate of change of a signal.", + "docstringHtml": "

Rate limiter block that limits the rate of change of a signal.

\n

Implements a continuous-time rate limiter as a first-order tracking system\nwith clipped rate of change:

\n
\n\\begin{equation*}\n\\dot{x} = \\mathrm{clip}\\left(f_\\mathrm{max} (u - x),\\; -r,\\; r\\right)\n\\end{equation*}\n
\n

where r is the maximum allowed rate and f_max controls the tracking\nbandwidth when the signal is not rate-limited. The output is the state\n\\(y = x\\).

\n
\n

Note

\n

The parameter f_max should be set high enough that the output tracks\nthe input without lag when the rate is within limits.

\n
\n
\n

Example

\n

The block is initialized like this:

\n
\n#max rate of 10 units/s\nrl = RateLimiter(rate=10.0, f_max=1e3)\n
\n
\n
\n

Parameters

\n
\n
rate : float
\n
maximum rate of change (positive value)
\n
f_max : float
\n
tracking bandwidth parameter
\n
\n
\n", + "params": { + "rate": { + "type": "number", + "default": "1.0", + "description": "maximum rate of change (positive value)" + }, + "f_max": { + "type": "integer", + "default": "100", + "description": "tracking bandwidth parameter" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] }, "TransferFunctionNumDen": { "blockClass": "TransferFunctionNumDen", @@ -757,7 +867,7 @@ export const extractedBlocks: Record = "Function": { "blockClass": "Function", "description": "Arbitrary MIMO function block, defined by a function or `lambda` expression.", - "docstringHtml": "

Arbitrary MIMO function block, defined by a function or lambda expression.

\n

The function can have multiple arguments that are then provided\nby the input channels of the function block.

\n

Form multi input, the function has to specify multiple arguments\nand for multi output, the aoutputs have to be provided as a\ntuple or list.

\n

In the context of the global system, this block implements algebraic\ncomponents of the global system ODE/DAE.

\n
\n\\begin{equation*}\n\\vec{y} = \\mathrm{func}(\\vec{u})\n\\end{equation*}\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.\nTherefore func must be purely algebraic and not introduce states,\ndelay, etc. For interfacing with external stateful APIs, use the\nWrapper block.

\n
\n
\n

Note

\n

If the outputs are provided as a single numpy array, they are\nconsidered a single output. For MIMO, output has to be tuple.

\n
\n
\n

Example

\n

consider the function:

\n
\nfrom pathsim.blocks import Function\n\ndef f(a, b, c):\n    return a**2, a*b, b/c\n\nfn = Function(f)\n
\n

then, when the block is updated, the input channels of the block are\nassigned to the function arguments following this scheme:

\n
\ninputs[0] -> a\ninputs[1] -> b\ninputs[2] -> c\n
\n

and the function outputs are assigned to the\noutput channels of the block in the same way:

\n
\na**2 -> outputs[0]\na*b  -> outputs[1]\nb/c  -> outputs[2]\n
\n

Because the Function block only has a single argument, it can be\nused to decorate a function and make it a PathSim block. This might\nbe handy in some cases to keep definitions concise and localized\nin the code:

\n
\nfrom pathsim.blocks import Function\n\n#does the same as the definition above\n\n@Function\ndef fn(a, b, c):\n    return a**2, a*b, b/c\n\n#'fn' is now a PathSim block\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
MIMO function that defines algebraic block IO behaviour, signature func(*tuple)
\n
\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator that wraps func
\n
\n
\n", + "docstringHtml": "

Arbitrary MIMO function block, defined by a function or lambda expression.

\n

The function can have multiple arguments that are then provided\nby the input channels of the function block.

\n

Form multi input, the function has to specify multiple arguments\nand for multi output, the aoutputs have to be provided as a\ntuple or list.

\n

In the context of the global system, this block implements algebraic\ncomponents of the global system ODE/DAE.

\n
\n\\begin{equation*}\n\\vec{y} = \\mathrm{func}(\\vec{u})\n\\end{equation*}\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.\nTherefore func must be purely algebraic and not introduce states,\ndelay, etc. For interfacing with external stateful APIs, use the\nWrapper block.

\n
\n
\n

Note

\n

If the outputs are provided as a single numpy array, they are\nconsidered a single output. For MIMO, output has to be tuple.

\n
\n
\n

Example

\n

consider the function:

\n
\nfrom pathsim.blocks import Function\n\ndef f(a, b, c):\n    return a**2, a*b, b/c\n\nfn = Function(f)\n
\n

then, when the block is updated, the input channels of the block are\nassigned to the function arguments following this scheme:

\n
\ninputs[0] -> a\ninputs[1] -> b\ninputs[2] -> c\n
\n

and the function outputs are assigned to the\noutput channels of the block in the same way:

\n
\na**2 -> outputs[0]\na*b  -> outputs[1]\nb/c  -> outputs[2]\n
\n

Because the Function block only has a single argument, it can be\nused to decorate a function and make it a PathSim block. This might\nbe handy in some cases to keep definitions concise and localized\nin the code:

\n
\nfrom pathsim.blocks import Function\n\n#does the same as the definition above\n\n@Function\ndef fn(a, b, c):\n    return a**2, a*b, b/c\n\n#'fn' is now a PathSim block\n
\n
\n
\n

Parameters

\n
\n
func : callable
\n
MIMO function that defines algebraic block IO behaviour, signature func(*tuple)
\n
\n
\n
\n

Attributes

\n
\n
op_alg : Operator
\n
internal algebraic operator that wraps func
\n
\n
\n", "params": { "func": { "type": "callable", @@ -1068,7 +1178,7 @@ export const extractedBlocks: Record = "threshold": { "type": "number", "default": "0.0", - "description": "threshold for zero crossing" + "description": "threshold for zero crossing Attributes ----------" } }, "inputs": [ @@ -1091,7 +1201,7 @@ export const extractedBlocks: Record = "threshold": { "type": "number", "default": "0.0", - "description": "threshold for zero crossing" + "description": "threshold for zero crossing Attributes ----------" } }, "inputs": [ @@ -1114,7 +1224,7 @@ export const extractedBlocks: Record = "threshold": { "type": "number", "default": "0.0", - "description": "threshold for zero crossing" + "description": "threshold for zero crossing Attributes ----------" } }, "inputs": [ @@ -1127,7 +1237,7 @@ export const extractedBlocks: Record = "Relay": { "blockClass": "Relay", "description": "Relay block with hysteresis (Schmitt trigger).", - "docstringHtml": "

Relay block with hysteresis (Schmitt trigger).

\n

Switches output between two values based on input crossing upper and lower\nthresholds. The hysteresis prevents rapid switching when input is noisy.

\n

When input rises above threshold_up, output switches to value_up.\nWhen input falls below threshold_down, output switches to value_down.

\n
\n

Examples

\n

Basic thermostat that turns heater on below 19°C, off above 21°C:

\n
\nfrom pathsim.blocks import Relay\n\nthermostat = Relay(\n    threshold_up=21.0,\n    threshold_down=19.0,\n    value_up=0.0,\n    value_down=1.0\n    )\n
\n
\n
\n

Parameters

\n
\n
threshold_up : float
\n
threshold for transitioning to upper relay state value_up (default: 1.0)
\n
threshold_down : float
\n
threshold for transitioning to lower relay state value_down (default: 0.0)
\n
value_up : float
\n
value for upper relay state (default: 1.0)
\n
value_down : float
\n
value for lower relay state (default: 0.0)
\n
\n
\n
\n

Attributes

\n
\n
events : list[ZeroCrossingUp, ZeroCrossingDown]
\n
internal zero crossing events for relay state transitions
\n
\n
\n", + "docstringHtml": "

Relay block with hysteresis (Schmitt trigger).

\n

Switches output between two values based on input crossing upper and lower\nthresholds. The hysteresis prevents rapid switching when input is noisy.

\n

When input rises above threshold_up, output switches to value_up.\nWhen input falls below threshold_down, output switches to value_down.

\n
\n

Examples

\n

Basic thermostat that turns heater on below 19°C, off above 21°C:

\n
\nfrom pathsim.blocks import Relay\n\nthermostat = Relay(\n    threshold_up=21.0,\n    threshold_down=19.0,\n    value_up=0.0,\n    value_down=1.0\n    )\n
\n
\n
\n

Parameters

\n
\n
threshold_up : float
\n
threshold for transitioning to upper relay state value_up (default: 1.0)
\n
threshold_down : float
\n
threshold for transitioning to lower relay state value_down (default: 0.0)
\n
value_up : float
\n
value for upper relay state (default: 1.0)
\n
value_down : float
\n
value for lower relay state (default: 0.0)
\n
\n
\n
\n

Attributes

\n
\n
events : list[ZeroCrossingUp, ZeroCrossingDown]
\n
internal zero crossing events for relay state transitions
\n
\n
\n", "params": { "threshold_up": { "type": "number", @@ -1184,7 +1294,7 @@ export const extractedBlocks: Record = "Spectrum": { "blockClass": "Spectrum", "description": "Block for fourier spectrum analysis (spectrum analyzer).", - "docstringHtml": "

Block for fourier spectrum analysis (spectrum analyzer).

\n

Computes continuous time running fourier transform (RFT) of the incoming signal.

\n

A time threshold can be set by 't_wait' to start recording data only after the\nsimulation time is larger then the specified waiting time, i.e. 't - t_wait > dt'.\nThis is useful for recording the steady state after all the transients have settled.

\n

An exponential forgetting factor 'alpha' can be specified for realtime spectral\nanalysis. It biases the spectral components exponentially to the most recent signal\nvalues by applying a single sided exponential window like this:

\n
\n\\begin{equation*}\n\\int_0^t u(\\tau) \\exp(\\alpha (t-\\tau)) \\exp(-j \\omega \\tau)\\ d \\tau\n\\end{equation*}\n
\n

It is also known as the 'exponentially forgetting transform' (EFT) and a form of\nshort time fourier transform (STFT). It is implemented as a 1st order statespace model

\n
\n\\begin{equation*}\n\\dot{x} = - \\alpha x + \\exp(-j \\omega t) u\n\\end{equation*}\n
\n

where 'u' is the input signal and 'x' is the state variable that represents the\ncomplex fourier coefficient to the frequency 'omega'. The ODE is integrated using the\nnumerical integration engine of the block.

\n
\n

Example

\n

This is how to initialize it:

\n
\nimport numpy as np\n\n#linear frequencies (0Hz, DC -> 1kHz)\nsp1 = Spectrum(\n    freq=np.linspace(0, 1e3, 100),\n    labels=['x1', 'x2'] #labels for two inputs\n    )\n\n#log frequencies (1Hz -> 1kHz)\nsp2 = Spectrum(\n    freq=np.logspace(0, 3, 100)\n    )\n\n#log frequencies including DC (0Hz, DC + 1Hz -> 1kHz)\nsp3 = Spectrum(\n    freq=np.hstack([0.0, np.logspace(0, 3, 100)])\n    )\n\n#arbitrary frequencies\nsp4 = Spectrum(\n    freq=np.array([0, 0.5, 20, 1e3])\n    )\n
\n
\n
\n

Note

\n

This block is relatively slow! But it is valuable for long running simulations\nwith few evaluation frequencies, where just FFT'ing the time series data\nwouldnt be efficient OR if only the evaluation at weirdly spaced frequencies\nis required. Otherwise its more efficient to just do an FFT on the time\nseries recording after the simulation has finished.

\n
\n
\n

Parameters

\n
\n
freq : array[float]
\n
list of evaluation frequencies for RFT, can be arbitrarily spaced
\n
t_wait : float
\n
wait time before starting RFT
\n
alpha : float
\n
exponential forgetting factor for realtime spectrum
\n
labels : list[str]
\n
labels for the inputs
\n
\n
\n", + "docstringHtml": "

Block for fourier spectrum analysis (spectrum analyzer).

\n

Computes continuous time running fourier transform (RFT) of the incoming signal.

\n

A time threshold can be set by 't_wait' to start recording data only after the\nsimulation time is larger then the specified waiting time, i.e. 't - t_wait > dt'.\nThis is useful for recording the steady state after all the transients have settled.

\n

An exponential forgetting factor 'alpha' can be specified for realtime spectral\nanalysis. It biases the spectral components exponentially to the most recent signal\nvalues by applying a single sided exponential window like this:

\n
\n\\begin{equation*}\n\\int_0^t u(\\tau) \\exp(\\alpha (t-\\tau)) \\exp(-j \\omega \\tau)\\ d \\tau\n\\end{equation*}\n
\n

It is also known as the 'exponentially forgetting transform' (EFT) and a form of\nshort time fourier transform (STFT). It is implemented as a 1st order statespace model

\n
\n\\begin{equation*}\n\\dot{x} = - \\alpha x + \\exp(-j \\omega t) u\n\\end{equation*}\n
\n

where 'u' is the input signal and 'x' is the state variable that represents the\ncomplex fourier coefficient to the frequency 'omega'. The ODE is integrated using the\nnumerical integration engine of the block.

\n
\n

Example

\n

This is how to initialize it:

\n
\nimport numpy as np\n\n#linear frequencies (0Hz, DC -> 1kHz)\nsp1 = Spectrum(\n    freq=np.linspace(0, 1e3, 100),\n    labels=['x1', 'x2'] #labels for two inputs\n    )\n\n#log frequencies (1Hz -> 1kHz)\nsp2 = Spectrum(\n    freq=np.logspace(0, 3, 100)\n    )\n\n#log frequencies including DC (0Hz, DC + 1Hz -> 1kHz)\nsp3 = Spectrum(\n    freq=np.hstack([0.0, np.logspace(0, 3, 100)])\n    )\n\n#arbitrary frequencies\nsp4 = Spectrum(\n    freq=np.array([0, 0.5, 20, 1e3])\n    )\n
\n
\n
\n

Note

\n

This block is relatively slow! But it is valuable for long running simulations\nwith few evaluation frequencies, where just FFT'ing the time series data\nwouldnt be efficient OR if only the evaluation at weirdly spaced frequencies\nis required. Otherwise its more efficient to just do an FFT on the time\nseries recording after the simulation has finished.

\n
\n
\n

Parameters

\n
\n
freq : array[float]
\n
list of evaluation frequencies for RFT, can be arbitrarily spaced
\n
t_wait : float
\n
wait time before starting RFT
\n
alpha : float
\n
exponential forgetting factor for realtime spectrum
\n
labels : list[str]
\n
labels for the inputs
\n
\n
\n", "params": { "freq": { "type": "array", @@ -1213,7 +1323,7 @@ export const extractedBlocks: Record = "Subsystem": { "blockClass": "Subsystem", "description": "Subsystem class that holds its own blocks and connecions and", - "docstringHtml": "

Subsystem class that holds its own blocks and connecions and\ncan natively interface with the main simulation loop.

\n

IO interface is realized by a special 'Interface' block, that has extra\nmethods for setting and getting inputs and outputs and serves\nas the interface of the internal blocks to the outside.

\n

The subsystem doesnt use its 'inputs' and 'outputs' dicts directly.\nIt exclusively handles data transfer via the 'Interface' block.

\n

This class can be used just like any other block during the simulation,\nsince it implements the required methods 'update' for the fixed-point\niteration (resolving algebraic loops with instant time blocks),\nthe 'step' method that performs timestepping (especially for dynamic\nblocks with internal states) and the 'solve' method for solving the\nimplicit update equation for implicit solvers.

\n
\n

Example

\n

This is how we can wrap up multiple blocks within a subsystem.\nIn this case vanderpol system built from discrete components\ninstead of using an ODE block (in practice you should use\na monolithic ODE whenever possible due to performance).

\n
\nfrom pathsim import Subsystem, Interface, Connection\nfrom pathsim.blocks import Integrator, Function\n\n#van der Pol parameter\nmu = 1000\n\n#blocks in the subsystem\nIf = Interface() # this is the interface to the outside\nI1 = Integrator(2)\nI2 = Integrator(0)\nFn = Function(lambda x1, x2: mu*(1 - x1**2)*x2 - x1)\n\nsub_blocks = [If, I1, I2, Fn]\n\n#connections in the subsystem\nsub_connections = [\n    Connection(I2, I1, Fn[1], If[1]),\n    Connection(I1, Fn, If),\n    Connection(Fn, I2)\n    ]\n\n#the subsystem acts just like a normal block\nvdp = Subsystem(sub_blocks, sub_connections)\n
\n
\n
\n

Parameters

\n
\n
blocks : list[Block] | None
\n
internal blocks of the subsystem
\n
connections : list[Connection] | None
\n
internal connections of the subsystem
\n
\n

events : list[Event] | None\ntolerance_fpi : float

\n
\nabsolute tolerance for convergence of algebraic loops\ndefault see ´SIM_TOLERANCE_FPI´ in ´_constants.py´
\n
\n
iterations_max : int
\n
maximum allowed number of iterations for algebraic loop\nsolver, default see ´SIM_ITERATIONS_MAX´ in ´_constants.py´
\n
\n
\n
\n

Attributes

\n
\n
interface : Interface
\n
internal interface block for data transfer to the outside
\n
graph : Graph
\n
internal graph representation for fast system funcion\nevluations using DAG with algebraic depths
\n
boosters : None | list[ConnectionBooster]
\n
list of boosters (fixed point accelerators) that wrap\nalgebraic loop closing connections assembled from the\nsystem graph
\n
\n
\n", + "docstringHtml": "

Subsystem class that holds its own blocks and connecions and\ncan natively interface with the main simulation loop.

\n

IO interface is realized by a special 'Interface' block, that has extra\nmethods for setting and getting inputs and outputs and serves\nas the interface of the internal blocks to the outside.

\n

The subsystem doesnt use its 'inputs' and 'outputs' dicts directly.\nIt exclusively handles data transfer via the 'Interface' block.

\n

This class can be used just like any other block during the simulation,\nsince it implements the required methods 'update' for the fixed-point\niteration (resolving algebraic loops with instant time blocks),\nthe 'step' method that performs timestepping (especially for dynamic\nblocks with internal states) and the 'solve' method for solving the\nimplicit update equation for implicit solvers.

\n
\n

Example

\n

This is how we can wrap up multiple blocks within a subsystem.\nIn this case vanderpol system built from discrete components\ninstead of using an ODE block (in practice you should use\na monolithic ODE whenever possible due to performance).

\n
\nfrom pathsim import Subsystem, Interface, Connection\nfrom pathsim.blocks import Integrator, Function\n\n#van der Pol parameter\nmu = 1000\n\n#blocks in the subsystem\nIf = Interface() # this is the interface to the outside\nI1 = Integrator(2)\nI2 = Integrator(0)\nFn = Function(lambda x1, x2: mu*(1 - x1**2)*x2 - x1)\n\nsub_blocks = [If, I1, I2, Fn]\n\n#connections in the subsystem\nsub_connections = [\n    Connection(I2, I1, Fn[1], If[1]),\n    Connection(I1, Fn, If),\n    Connection(Fn, I2)\n    ]\n\n#the subsystem acts just like a normal block\nvdp = Subsystem(sub_blocks, sub_connections)\n
\n
\n
\n

Parameters

\n
\n
blocks : list[Block] | None
\n
internal blocks of the subsystem
\n
connections : list[Connection] | None
\n
internal connections of the subsystem
\n
\n

events : list[Event] | None\ntolerance_fpi : float

\n
\nabsolute tolerance for convergence of algebraic loops\ndefault see ´SIM_TOLERANCE_FPI´ in ´_constants.py´
\n
\n
iterations_max : int
\n
maximum allowed number of iterations for algebraic loop\nsolver, default see ´SIM_ITERATIONS_MAX´ in ´_constants.py´
\n
\n
\n
\n

Attributes

\n
\n
interface : Interface
\n
internal interface block for data transfer to the outside
\n
graph : Graph
\n
internal graph representation for fast system funcion\nevluations using DAG with algebraic depths
\n
boosters : None | list[ConnectionBooster]
\n
list of boosters (fixed point accelerators) that wrap\nalgebraic loop closing connections assembled from the\nsystem graph
\n
\n
\n", "params": {}, "inputs": [], "outputs": [] @@ -1248,7 +1358,10 @@ export const extractedBlocks: Record = } }, "inputs": null, - "outputs": null + "outputs": [ + "x", + "x/tau" + ] }, "Bubbler4": { "blockClass": "Bubbler4", @@ -1271,8 +1384,17 @@ export const extractedBlocks: Record = "description": "Times at which each vial is replaced with a fresh one. If None, no replacement events are created. If a single value is provided, it is used for all vials. If a single list of floats is provided, it will be used for all vials. If a list of lists is provided, each sublist corresponds to the replacement times for each vial." } }, - "inputs": null, - "outputs": null + "inputs": [ + "sample_in_soluble", + "sample_in_insoluble" + ], + "outputs": [ + "vial1", + "vial2", + "vial3", + "vial4", + "sample_out" + ] }, "Splitter": { "blockClass": "Splitter", @@ -1285,7 +1407,9 @@ export const extractedBlocks: Record = "description": "fractions to split the input signal into, must sum up to one" } }, - "inputs": null, + "inputs": [ + "in" + ], "outputs": null }, "GLC": { @@ -1329,14 +1453,28 @@ export const extractedBlocks: Record = "description": "BCs: Boundary conditions type, \"C-C\" (Closed-Closed) or \"O-C\" (Open-Closed), default is \"C-C\"" } }, - "inputs": null, - "outputs": null + "inputs": [ + "c_T_in", + "flow_l", + "y_T2_inlet", + "flow_g" + ], + "outputs": [ + "c_T_out", + "y_T2_out", + "eff", + "P_out", + "Q_l", + "Q_g_out", + "n_T_out_liquid", + "n_T_out_gas" + ] } }; 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", "PID", "AntiWindupPID", "TransferFunctionNumDen", "TransferFunctionZPG", "ButterworthLowpassFilter", "ButterworthHighpassFilter", "ButterworthBandpassFilter", "ButterworthBandstopFilter"], + Dynamic: ["Integrator", "Differentiator", "Delay", "ODE", "DynamicalSystem", "StateSpace", "PT1", "PT2", "LeadLag", "PID", "AntiWindupPID", "RateLimiter", "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"], Mixed: ["SampleHold", "FIR", "ADC", "DAC", "Counter", "CounterUp", "CounterDown", "Relay"], Recording: ["Scope", "Spectrum"], @@ -1372,20 +1510,23 @@ export const blockImportPaths: Record = { "GLC": "pathsim_chem.tritium", "GaussianPulseSource": "pathsim.blocks", "Integrator": "pathsim.blocks", - "Interface": "pathsim.blocks", "LUT": "pathsim.blocks", "LUT1D": "pathsim.blocks", + "LeadLag": "pathsim.blocks", "Log": "pathsim.blocks", "Log10": "pathsim.blocks", "Mod": "pathsim.blocks", "Multiplier": "pathsim.blocks", "ODE": "pathsim.blocks", "PID": "pathsim.blocks", + "PT1": "pathsim.blocks", + "PT2": "pathsim.blocks", "PinkNoise": "pathsim.blocks", "Pow": "pathsim.blocks", "Process": "pathsim_chem.tritium", "PulseSource": "pathsim.blocks", "RandomNumberGenerator": "pathsim.blocks", + "RateLimiter": "pathsim.blocks", "Relay": "pathsim.blocks", "SampleHold": "pathsim.blocks", "Scope": "pathsim.blocks", @@ -1398,7 +1539,6 @@ export const blockImportPaths: Record = { "SquareWaveSource": "pathsim.blocks", "StateSpace": "pathsim.blocks", "StepSource": "pathsim.blocks", - "Subsystem": "pathsim.blocks", "Switch": "pathsim.blocks", "Tan": "pathsim.blocks", "Tanh": "pathsim.blocks", diff --git a/src/lib/nodes/uiConfig.ts b/src/lib/nodes/uiConfig.ts index a697c86d..b5ca3337 100644 --- a/src/lib/nodes/uiConfig.ts +++ b/src/lib/nodes/uiConfig.ts @@ -63,9 +63,6 @@ export const syncPortBlocks = new Set([ 'Integrator', 'Differentiator', 'Delay', - 'PID', - 'PID_Antiwindup', - // Algebraic blocks (element-wise operations) 'Amplifier', 'Sin', From 1e7ed16e56513369433f57e80bb39a62c1a533eb Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 16 Feb 2026 14:10:03 +0100 Subject: [PATCH 2/2] Add Deadband and Backlash blocks --- scripts/config/pathsim/blocks.json | 2 ++ scripts/generated/registry.json | 16 ++++++++++ src/lib/nodes/generated/blocks.ts | 50 +++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/scripts/config/pathsim/blocks.json b/scripts/config/pathsim/blocks.json index 4163e87e..bcc5e34e 100644 --- a/scripts/config/pathsim/blocks.json +++ b/scripts/config/pathsim/blocks.json @@ -33,6 +33,8 @@ "PID", "AntiWindupPID", "RateLimiter", + "Backlash", + "Deadband", "TransferFunctionNumDen", "TransferFunctionZPG", "ButterworthLowpassFilter", diff --git a/scripts/generated/registry.json b/scripts/generated/registry.json index 67027734..cf3fb206 100644 --- a/scripts/generated/registry.json +++ b/scripts/generated/registry.json @@ -227,6 +227,22 @@ "f_max" ] }, + "Backlash": { + "blockClass": "Backlash", + "importPath": "pathsim.blocks", + "params": [ + "width", + "f_max" + ] + }, + "Deadband": { + "blockClass": "Deadband", + "importPath": "pathsim.blocks", + "params": [ + "lower", + "upper" + ] + }, "TransferFunctionNumDen": { "blockClass": "TransferFunctionNumDen", "importPath": "pathsim.blocks", diff --git a/src/lib/nodes/generated/blocks.ts b/src/lib/nodes/generated/blocks.ts index dfd5d184..af8c6fe8 100644 --- a/src/lib/nodes/generated/blocks.ts +++ b/src/lib/nodes/generated/blocks.ts @@ -681,6 +681,52 @@ export const extractedBlocks: Record = "out" ] }, + "Backlash": { + "blockClass": "Backlash", + "description": "Backlash (mechanical play) element.", + "docstringHtml": "

Backlash (mechanical play) element.

\n

Models the hysteresis-like behavior of mechanical backlash in gears,\ncouplings and other systems with play. The output only tracks the input\nafter the input has moved through the full backlash width.

\n
\n\\begin{equation*}\n\\dot{x} = f_\\mathrm{max} \\left((u - x) - \\mathrm{clip}(u - x,\\; -w/2,\\; w/2)\\right)\n\\end{equation*}\n
\n

where w is the total backlash width. Inside the dead zone \\(|u - x| \\leq w/2\\)\nthe output does not move. Once the input pushes past the edge, the output\ntracks with bandwidth f_max.

\n
\n

Example

\n

The block is initialized like this:

\n
\n#backlash with 0.5 units of total play\nbl = Backlash(width=0.5, f_max=1e3)\n
\n
\n
\n

Parameters

\n
\n
width : float
\n
total backlash width (play)
\n
f_max : float
\n
tracking bandwidth parameter when engaged
\n
\n
\n", + "params": { + "width": { + "type": "number", + "default": "1.0", + "description": "total backlash width (play)" + }, + "f_max": { + "type": "integer", + "default": "100", + "description": "tracking bandwidth parameter when engaged" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, + "Deadband": { + "blockClass": "Deadband", + "description": "Deadband (dead zone) element.", + "docstringHtml": "

Deadband (dead zone) element.

\n

Outputs zero when the input is within the dead zone, and passes\nthe signal shifted by the zone boundary otherwise:

\n
\n\\begin{equation*}\ny = \\begin{cases}\n u - u_\\mathrm{upper} & \\text{if } u > u_\\mathrm{upper} \\\\\n 0 & \\text{if } u_\\mathrm{lower} \\leq u \\leq u_\\mathrm{upper} \\\\\n u - u_\\mathrm{lower} & \\text{if } u < u_\\mathrm{lower}\n\\end{cases}\n\\end{equation*}\n
\n

or equivalently \\(y = u - \\mathrm{clip}(u,\\; u_\\mathrm{lower},\\; u_\\mathrm{upper})\\).

\n
\n

Example

\n

The block is initialized like this:

\n
\n#symmetric dead zone of width 0.2\ndb = Deadband(lower=-0.1, upper=0.1)\n
\n
\n
\n

Parameters

\n
\n
lower : float
\n
lower bound of the dead zone
\n
upper : float
\n
upper bound of the dead zone
\n
\n
\n", + "params": { + "lower": { + "type": "number", + "default": "-1.0", + "description": "lower bound of the dead zone" + }, + "upper": { + "type": "number", + "default": "1.0", + "description": "upper bound of the dead zone" + } + }, + "inputs": [ + "in" + ], + "outputs": [ + "out" + ] + }, "TransferFunctionNumDen": { "blockClass": "TransferFunctionNumDen", "description": "This block defines a LTI (SISO) transfer function.", @@ -1474,7 +1520,7 @@ 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", "TransferFunctionNumDen", "TransferFunctionZPG", "ButterworthLowpassFilter", "ButterworthHighpassFilter", "ButterworthBandpassFilter", "ButterworthBandstopFilter"], + 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"], Mixed: ["SampleHold", "FIR", "ADC", "DAC", "Counter", "CounterUp", "CounterDown", "Relay"], Recording: ["Scope", "Spectrum"], @@ -1487,6 +1533,7 @@ export const blockImportPaths: Record = { "Adder": "pathsim.blocks", "Amplifier": "pathsim.blocks", "AntiWindupPID": "pathsim.blocks", + "Backlash": "pathsim.blocks", "Bubbler4": "pathsim_chem.tritium", "ButterworthBandpassFilter": "pathsim.blocks", "ButterworthBandstopFilter": "pathsim.blocks", @@ -1501,6 +1548,7 @@ export const blockImportPaths: Record = { "CounterDown": "pathsim.blocks", "CounterUp": "pathsim.blocks", "DAC": "pathsim.blocks", + "Deadband": "pathsim.blocks", "Delay": "pathsim.blocks", "Differentiator": "pathsim.blocks", "DynamicalSystem": "pathsim.blocks",