{ "cells": [ { "cell_type": "markdown", "id": "500b4521", "metadata": {}, "source": [ "[← Modules](../../../getting_started/theory_to_python/modules.rst)" ] }, { "cell_type": "markdown", "id": "07f18cde", "metadata": { "lines_to_next_cell": 0 }, "source": [ "# DynamicalSystem\n", "\n", "The `DynamicalSystem` class is the foundational abstraction in `pykal` for modeling *any* control-system component (plant, controller, observer, sensor, estimator, etc.). This notebook demonstrates the core usage patterns of `DynamicalSystem` through progressively more complex examples. But first, the \"Conceptual Foundation\" section will cover the mathematical and programmatic foundation behind the `DynamicalSystem` class. **Do not skip this section**; reading it will solve a hundred problems before they start!\n", "\n", "## Conceptual Foundation\n", "We begin with the minimal mathematics needed to define the discrete-time dynamical systems. We then introduce notational conventions and end with a Python implementation of the concepts discussed.\n", "\n", "### Discrete-Time Dynamical Systems\n", ":::{note} This is a minimal category-theoretic definition of the discrete-time dynamical system. For an intuitive overview, including how to case continous-time dynamical systems as discrete-time dynamical systems, check out [Steve Brunton's video](https://www.youtube.com/watch?v=0dvF2jesB4E).\n", "For a reference to the category theoretic concepts needed here (which are quite minimal), check out the relevant wikipedia article on the category [$\\text{Set}$](https://en.wikipedia.org/wiki/Category_of_sets).\n", ":::\n", "\n", "Consider the category $\\text{Set}$. Let $T \\in \\text{Set}$ where $T \\subset \\mathbb{R}^+ $ and $T$ has finite cardinality $|T| = N$. We will refer to such a set as a **time domain**. Given a time domain $T$, we may construct an **indexing set** $K=\\left\\{ 1,2,\\dots,N\\right\\}$, where $ k \\in K$ indexes elements of our time domain $T$ as $t_k$. In so doing, $T$ becomes an **indexed time domain**.\n", "\n", " Now let $X, Y, \\mathcal{M} \\in \\text{Set}$. We will refer to $X$ as the **state space**, $Y$ as the **output space**, and $\\mathcal{M}$ the **parameter space**. We place the following restriction on our choice of $X,Y$, and $\\mathcal{M}$:\n", "\n", "- No space in the triple $(X,Y,\\mathcal{M})$ is empty.\n", "- At least one space in the triple $(X,Y,\\mathcal{M})$ must have $|T| = N$ elements.\n", "\n", "We will now construct a special function that will map a subset of $X \\times \\mathcal{M}$ with a subset of $X \\times Y$. We will call such a function a **discrete-time dynamical system.**\n", "\n", "First, consider the product $X \\times \\mathcal{M} \\times X \\times Y$. Such a product will clearly have cardinality $|X| \\cdot |\\mathcal{M}| \\cdot |X| \\cdot |Y| \\geq N$ (by the restriction above); thus, we may arbitrarily index a subset of the product $X \\times \\mathcal{M} \\times X \\times Y$ using the indexing set $K-1:=\\left\\{ 1,2,\\dots,N-1\\right\\}$ with indices $k$ as follows:\n", "\n", "$$\n", "(x_k,\\mu_k,x_{k+1},y_j) \\in X \\times \\mathcal{M} \\times X \\times Y.\n", "$$\n", "\n", "We may split each tuple $(x_k,\\mu_k,x_{k+1},y_k)$ into a tuple of tuples $((x_k,\\mu_k),(x_{k+1},y_j))$, where, by the ordered pair definition of a function, we have created a map\n", "\n", "$$\n", "(x_k,\\mu_k) \\mapsto (x_{k+1},y_k)\n", "$$\n", "\n", "We may define such a map as the tuple of functions:\n", "\n", "$$\n", "(f, h)(x_k,\\mu_k) = (x_{k+1},y_k)\n", "$$\n", "\n", "where\n", "\n", "$$\n", "\\boxed{\n", "(f, h) (x_k,\\mu_k) := (f(x_k,\\mu_k),h(x_k,\\mu_k))\n", "}\n", "$$\n", "\n", "and\n", "\n", "$$\n", "\\boxed{\n", "f(x_k,\\mu_k) = x_{k+1} ,\n", "\\qquad\n", "h(x_k,\\mu_k) = y_k.\n", "}\n", "$$\n", "\n", "The tuple of functions $(f,h)$ constructed above shall be referred to as a **discrete-time dynamical system**. We will refer to $f$ as our **evolution function** (or **dynamics**), and $h$ as our **output function** (or **measurement** function, based on context). Finally, the parameter $x_k$ is often referred to as the **state** of a dynamical system, and $y_k$ is referred to as the **output** or **measurement** (again, depending on context)\n", "\n", ":::{note} Our evolution and output functions are *defined* by our choice of $(X,Y,\\mathcal{M})$, our choice of time domain $T$, and our choice of indexing $(x_k,\\mu_k,x_{k+1},y_k)$. Since all of these choices are arbitrary, we have defined *every possible* discrete-time dynamical system one could possible imagine (via the axiom of choice). All that is left is to implement them.\n", ":::\n", "### Simplifying Conventions\n", "Let $(f,h)$ be defined as above. We shall refer to the elements $(x_k,\\mu_k)$ as **parameter tuples** and the variable in each slot as **parameters**.\n", "\n", "For example, if\n", "\n", "$$\n", "(f,h)(x_k,\\mu_k)=(x_{k+1},\\mu_k)\n", "$$\n", "\n", " then $(x_k,\\mu_k)$ is a parameter tuple and $x_k$ and $\\mu_k$ are our parameters. But if $\\mu_k = (t_k,a_k,b_k,c_k)$, then\n", "\n", "$$\n", "(f,h)(x_k,\\mu_k) = (f,h)(x_k,(t_k,a_k,b_k,c_k)) =: (f,h)(x_k,t_k,a_k,b_k,c_k)\n", "$$\n", "\n", "so our parameter tuple is $(x_k,t_k,a_k,b_k,c_k)$ and our parameters are $x_k,t_k,a_k,b_k,$ and $c_k$.\n", "\n", "Going forward, we rarely discuss parameter tuples and almost exclusively focus on parameters. We will also find that the number of parameters for a given dynamical system can be considerable; therefore, we introduce the following convention.\n", "\n", "Suppose that, in the example above, we decide that only the parameters $x_k$ and $t_k$ are worth explicitely writing down. Then we may write\n", "\n", "$$\n", "(f,h)(x_k,t_k,a_k,b_k,c_k) = (f,h)(x_k,t_k,\\dots)\n", "$$\n", "\n", "where we call $x_k$ and $a_k$ **explicit parameters**, and $a_k$,$b_k$ and $c_k$ are **implicit parameters** which we infer from context (think of them as \"hiding\" in the ellipses). As a general guideline: if they exists, $x_k$ and $t_k$ should always be explicit parameters.\n", "\n", ":::{note} With the exception of the guideline given above, it is up to the users discretion which parameters should be explicit and which should be implicit. There is no mathematical difference or even implementation difference in Python; writing some parameters implicitly via ellipses is just a shorthand.\n", ":::\n", "### Programmatic Implementation of a Discrete-Time Dynamical System\n", "Recall that our discrete-time dynamical system is given by a tuple of functions $(f,h)$ subject to the restrictions previously mentioned. Often, we will refer to such a tuple $(f,h)$ as the **$(f,h)$-representation** of a discrete-time dynamical system.\n", "\n", "Now, we discuss the `pykal.DynamicalSystem` class, or `DynamicalSystem` class, for short. The constructor for the class binds the $(f,h)$-representation of a dynamical system to a `DynamicalSystem` object.\n", "\n", "Consider the following example, where we bind the $(f,h)$ representation of a standard exponential growth model." ] }, { "cell_type": "code", "execution_count": null, "id": "63901e6e", "metadata": {}, "outputs": [], "source": [ "from pykal import DynamicalSystem\n", "\n", "\n", "def f(xk: float, rate: float = 1):\n", " \"\"\"\n", " Discrete-time exponential growth model.\n", "\n", " x_{k+1} = rate * x_k\n", " \"\"\"\n", " xk_next = rate * xk\n", " return xk_next\n", "\n", "\n", "def h(xk: float):\n", " \"\"\"\n", " Measurement model (identity and scaled by log 10).\n", " \"\"\"\n", " return np.log10(xk)\n", "\n", "\n", "# Although mathematically, f and h have the same parameters, in our software implementation we need only include those paramters for f and h that they use. This is for convenience. See \"Parameter Binding\" for more.\n", "\n", "\n", "exp_growth = DynamicalSystem(f=f, h=h)\n", "exp_growth.__dict__" ] }, { "cell_type": "markdown", "id": "c3c75042", "metadata": { "lines_to_next_cell": 0 }, "source": [ ":::{note}\n", "Although mathematically $f$ and $h$ have the same parameters, in our Python definitions of `f` and `h` we need only include those paramters which are actually used by the functions. This is for convenience and readability; see \"Parameter Binding\" for more." ] }, { "cell_type": "markdown", "id": "9e43c25b", "metadata": { "lines_to_next_cell": 0 }, "source": [ "\n", "Now that we have bound the $(f,h)$ representation, we need a way to call it. Recall from the preceding sections that we have\n", "\n", "$$\n", "(f, h)(x_k,\\mu_k) = (x_{k+1},y_k)\n", "$$\n", "\n", "The `DynamicalSystem` object we've created has only a single method, called `step()`. The `step()` method has a keyword argument `params=` which expects an object of type `Dict`. When `step(params=Dict)` is called, the `DynamicalSystem` object passes the parameters to the bound $(f,h)$ and computes $(f,h)(...) = (x_{k+1},y_k)$.\n", "\n", "Consider the following simulation of exponential growth." ] }, { "cell_type": "code", "execution_count": null, "id": "e735add6", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import numpy as np\n", "\n", "dt = 1.0\n", "T = np.arange(0.0, 30.0, dt) # time domain\n", "\n", "X = []\n", "Y = []\n", "M = []\n", "\n", "xk = 1 # initial condition\n", "rate = 2\n", "for tk in T[:-1]:\n", " xk_next, yk = exp_growth.step(params={\"xk\": xk, \"rate\": rate})\n", "\n", " X.append(xk)\n", " Y.append(yk)\n", " M.append([xk, rate])\n", "\n", " xk = xk_next" ] }, { "cell_type": "code", "execution_count": null, "id": "1e18bcb5", "metadata": { "lines_to_next_cell": 0, "tags": [ "hide-input" ], "title": "# Visualize" }, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "a0173840", "metadata": { "tags": [ "hide-input" ], "title": "# Visualize" }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "# Convert to arrays\n", "X = np.asarray(X)\n", "Y = np.asarray(Y)\n", "M = np.asarray(M)\n", "\n", "# Time alignment: X[k], Y[k], M[k] correspond to T[k+1]\n", "T_plot = T[1:]\n", "\n", "# Create ONE figure with THREE plots\n", "fig = plt.figure(figsize=(12, 7))\n", "\n", "# ---- Top-left: State ----\n", "ax1 = fig.add_subplot(2, 2, 1)\n", "ax1.plot(T_plot, X, marker=\"o\")\n", "ax1.set_title(\"State: Exponential Growth\")\n", "ax1.set_xlabel(\"Time\")\n", "ax1.set_ylabel(r\"$x_k$\")\n", "ax1.grid(True)\n", "\n", "# ---- Top-right: Measurement ----\n", "ax2 = fig.add_subplot(2, 2, 2)\n", "ax2.plot(T_plot, Y, marker=\"o\")\n", "ax2.set_title(\"Measurement: Log-Scaled\")\n", "ax2.set_xlabel(\"Time\")\n", "ax2.set_ylabel(r\"$y_k = \\log_{10}(x_k)$\")\n", "ax2.grid(True)\n", "\n", "# ---- Bottom: x_k and rate ----\n", "ax3 = fig.add_subplot(2, 1, 2)\n", "ax3.plot(T_plot, M[:, 0], label=r\"$x_k$\", marker=\"o\")\n", "ax3.plot(T_plot, M[:, 1], label=r\"$\\text{rate}$\", linestyle=\"--\")\n", "ax3.set_title(\"Parameters\")\n", "ax3.set_xlabel(\"Time\")\n", "ax3.set_ylabel(\"Value\")\n", "ax3.legend()\n", "ax3.grid(True)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "8c12075c", "metadata": { "lines_to_next_cell": 0 }, "source": [ "## References and Examples\n", "\n", "The power of the $(f,h)$ abstraction lies in its flexibility and composability. In the following sections, we'll explore:\n", "\n", "- **Stateless systems vs Stateful systems**\n", "- **Single step vs simulation loops**\n", "- **Parameter Binding**: (enables flexibility when defining `f` and `h`)\n", "- **Composing Systems**\n" ] }, { "cell_type": "markdown", "id": "47bd2a26", "metadata": { "lines_to_next_cell": 2 }, "source": [ "### Stateless vs Stateful\n", "\n", "The `DynamicalSystem` constructor accepts two parameters: `f` and `h`. However, the 'f' function is optional, and excluding it is occasionally useful." ] }, { "cell_type": "markdown", "id": "e6dbc9e6", "metadata": { "lines_to_next_cell": 2 }, "source": [ "#### Stateless System (h only)\n", "\n", "A stateless system has no internal state evolution; that is, if we consider the $(f,h)$ representation, then $f$ is the null function ($f: \\varnothing \\rightarrow \\varnothing$). You would be right to question why we should even bother encapsulating such a system in a `DynamicalSystem` object; indeed, we could simply call the function 'h' directly.\n", "\n", "However, 'DynamicalSystem' objects can be useful for initial control system modeling, and, as we will see in ROS [link], if we want to create a stateless transformation node, then we need to first define a stateless `DynamicalSystem` object." ] }, { "cell_type": "code", "execution_count": null, "id": "4d3a2bae", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "# Example: Nonlinear sensor that squares its input\n", "def sensor_h(raw_signal: float) -> float:\n", " \"\"\"Quadratic sensor response\"\"\"\n", " return raw_signal**2\n", "\n", "\n", "sensor = DynamicalSystem(h=sensor_h)\n", "\n", "\n", "print(f\"sensor.f = {sensor.f}\")\n", "\n", "# Step just calls h\n", "output = sensor.step(params={\"raw_signal\": 3.0})\n", "print(f\"\\nSensor output: {output}\")" ] }, { "cell_type": "markdown", "id": "73b8336a", "metadata": { "lines_to_next_cell": 2 }, "source": [ "#### Stateful System\n", "\n", "This is what comes to mind when we think of dynamical systems. A stateful system maintains internal \"memory\" (i.e. state) and evolves over time. This is where the $(f,h)$ representation shines. The stateful system pattern is the most common for\n", "\n", "- Dynamical systems with physics\n", "- Controllers with integral/derivative terms\n", "- Observers/estimators\n", "- Any system with memory" ] }, { "cell_type": "code", "execution_count": null, "id": "7b77eb68", "metadata": {}, "outputs": [], "source": [ "# Example: Simple integrator (accumulator)\n", "def integrator_f(integral_state: float, input_signal: float, dt: float) -> float:\n", " \"\"\"Integrate input signal over time\"\"\"\n", " return integral_state + input_signal * dt\n", "\n", "\n", "def integrator_h(integral_state: float) -> float:\n", " \"\"\"Output the accumulated integral\"\"\"\n", " return integral_state\n", "\n", "\n", "integrator = DynamicalSystem(f=integrator_f, h=integrator_h)\n", "\n", "\n", "print(f\"\\nintegrator.f = {integrator.f}\")\n", "print(f\"\\nintegrator.h = {integrator.h}\")" ] }, { "cell_type": "markdown", "id": "b5ac19a6", "metadata": {}, "source": [ "::: {tip}\n", "States and outputs are **not** restricted to numbers and vectors. They can be vectors, matrices, tuples, functions, classes, neural networks, strings, or any other Python objects. Just make sure you add function annotations so it is clear what type of objects `f` and `h` expect.\n", ":::" ] }, { "cell_type": "markdown", "id": "780be1cf", "metadata": { "lines_to_next_cell": 2 }, "source": [ "### Single Step vs Simulation Loops\n", "\n", "The `step()` method calls the $(f,h)$ function with the appropriate parameters. You can think of it as executing one \"step\" of an iteration." ] }, { "cell_type": "markdown", "id": "a4ec467b", "metadata": { "lines_to_next_cell": 0 }, "source": [ "#### Single Step" ] }, { "cell_type": "code", "execution_count": null, "id": "33f00b26", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "# Simple exponential decay: x_{k+1} = alpha * x_k\n", "def decay_f(xk: float, alpha: float) -> float:\n", " return alpha * xk\n", "\n", "\n", "def decay_h(xk):\n", " return xk\n", "\n", "\n", "decay_system = DynamicalSystem(f=decay_f, h=decay_h)\n", "\n", "# Step\n", "xk = 10\n", "xk_next, yk = decay_system.step(params={\"xk\": xk, \"alpha\": 0.9})\n", "print(f\"Initial state: {xk}\")\n", "print(f\"Initial observation: {yk}\")\n", "print(f\"State after 1 step: {xk_next}\")" ] }, { "cell_type": "markdown", "id": "cc48838e", "metadata": { "lines_to_next_cell": 0 }, "source": [ "#### Simulation loop" ] }, { "cell_type": "code", "execution_count": null, "id": "5feaf909", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "# Simulate exponential decay for 50 steps\n", "xk = 10.0\n", "alpha = 0.9\n", "steps = 50\n", "\n", "X = []\n", "# Y = [] we can include these if we wish\n", "# M = []\n", "for k in range(steps):\n", " xk_next, y = decay_system.step(params={\"xk\": xk, \"alpha\": alpha})\n", "\n", " X.append(xk)\n", " # Y.append(yk)\n", " # M.append(alpha) # in this case, we just want to keep track of our paramter alpha\n", "\n", " xk = xk_next" ] }, { "cell_type": "code", "execution_count": null, "id": "851bd149", "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "X = np.asarray(X)\n", "# Visualize\n", "plt.figure(figsize=(10, 4))\n", "plt.plot(X, \"b-\", linewidth=2, label=f\"x(k), α={alpha}\")\n", "plt.xlabel(\"Time Step (k)\", fontsize=12)\n", "plt.ylabel(\"State Value\", fontsize=12)\n", "plt.title(\"Exponential Decay System\", fontsize=14, fontweight=\"bold\")\n", "plt.grid(True, alpha=0.3)\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "542e6f16", "metadata": { "lines_to_next_cell": 0 }, "source": [ ":::{warning}\n", "Be mindful of the order in which we call things in the simulation: first `(f,h)` is called, then `X` and `Y` are updated, and finally `xk` is set to `xk_next`. This ensures we don't append `xk_next` beyond the range of the simulation." ] }, { "cell_type": "markdown", "id": "dfb32cee", "metadata": { "lines_to_next_cell": 0 }, "source": [ "### Parameter Binding\n", "\n", "As you have likely noticed, although mathematically the parameters for $f$ and $h$ are the same, programmatically `f` and `h` declare different parameters. What is happening is that the `.step(params=Dict)` method has a common `params` dictionary, and relevant parameters for either `f` or `h` are routed internally. This is very convenient if parameters are duplicated across functions and makes for more readable code.\n" ] }, { "cell_type": "markdown", "id": "4a62220f", "metadata": { "lines_to_next_cell": 2 }, "source": [ "#### Positional vs Keyword Arguments\n", "\n", "Functions can use any combination of positional and keyword arguments. The `.step()` method does not discriminate between them." ] }, { "cell_type": "markdown", "id": "opur6r70iu", "source": "#### The `_smart_call` Function\n\nUnder the hood, the `.step()` method uses a static method called `_smart_call()` to route parameters from the shared `params` dictionary to the appropriate function arguments. This function inspects the signature of `f` or `h`, extracts only the parameters each function needs from the dictionary, and calls the function with those parameters.\n\nWhile `_smart_call()` is used internally by `.step()`, advanced users can call it directly when implementing complex algorithms. For example, the [Kalman Filter implementation](../../algorithm_library/kf_pykal.ipynb) uses `_smart_call()` within its own `f` function to invoke the plant's evolution and measurement functions with the correct parameters.\n\nFor technical details on how parameter binding works, see the [API reference for `_smart_call`](../../api/dynamical_system.rst#pykal.DynamicalSystem._smart_call).", "metadata": {} }, { "cell_type": "code", "execution_count": null, "id": "e99e3f94", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "# Different signature styles\n", "def style_1(x: float, u: float) -> float:\n", " \"\"\"Positional or keyword arguments\"\"\"\n", " return x + u\n", "\n", "\n", "def style_2(x: float, *, u: float) -> float:\n", " \"\"\"Keyword-only argument (u must be named)\"\"\"\n", " return x + u\n", "\n", "\n", "def style_3(x: float, u: float, dt: float = 0.1) -> float:\n", " \"\"\"With default value\"\"\"\n", " return x + u * dt\n", "\n", "\n", "def identity_h(x):\n", " return x\n", "\n", "\n", "sys1 = DynamicalSystem(f=style_1, h=identity_h)\n", "sys2 = DynamicalSystem(f=style_2, h=identity_h)\n", "sys3 = DynamicalSystem(f=style_3, h=identity_h)\n", "\n", "params = {\"x\": 1.0, \"u\": 2.0, \"dt\": 0.5}\n", "\n", "# All work with .step()!\n", "print(f\"Style 1 result: {sys1.step(params=params)[0]}\") # just print the state\n", "print(f\"Style 2 result: {sys2.step(params=params)[0]}\")\n", "print(f\"Style 3 result: {sys3.step(params=params)[0]}\")\n", "\n", "# If we omit dt, style_3 uses default (hence, a kwarg proved useful here)\n", "params_no_dt = {\"x\": 1.0, \"u\": 2.0}\n", "print(f\"Style 3 with default dt: {sys3.step(params=params_no_dt)[0]}\")" ] }, { "cell_type": "markdown", "id": "bed469d6", "metadata": { "lines_to_next_cell": 2 }, "source": [ "#### Using only **kwargs\n", "\n", "Functions with `**kwargs` will accept **all** parameters from the `params` dictionary (however, they will need to be extracted by name). This can be a convenient way of defining functions if you don't want to bother with updating function signatures as you change the parameters the function will use." ] }, { "cell_type": "code", "execution_count": null, "id": "88cbf7f9", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "# Flexible function that accepts any parameters\n", "def flexible_h(**kwargs) -> float:\n", " \"\"\"Use any available parameters\"\"\"\n", " # Extract what we need, with defaults\n", " a = kwargs.get(\"a\", 1.0)\n", " b = kwargs.get(\"b\", 0.0)\n", " c = kwargs.get(\"c\", 0.0)\n", " x = kwargs.get(\"x\", 0.0)\n", " return a * x + b * x**2 + c\n", "\n", "\n", "flexible_sys = DynamicalSystem(h=flexible_h)\n", "\n", "# Can provide different parameter combinations (since we have defaults in the definition)\n", "result1 = flexible_sys.step(params={\"x\": 2.0, \"a\": 3.0})\n", "result2 = flexible_sys.step(params={\"x\": 2.0, \"a\": 3.0, \"b\": 0.5})\n", "result3 = flexible_sys.step(params={\"x\": 2.0, \"a\": 3.0, \"b\": 0.5, \"c\": 1.0})\n", "\n", "print(f\"Result with a only: {result1}\")\n", "print(f\"Result with a, b: {result2}\")\n", "print(f\"Result with a, b, c: {result3}\")" ] }, { "cell_type": "markdown", "id": "adeb5f93", "metadata": { "lines_to_next_cell": 2 }, "source": [ "#### Parameter Dictionary Sharing\n", "\n", "Multiple systems can share a common parameter dictionary. The `.step()` method extracts only what each function needs." ] }, { "cell_type": "code", "execution_count": null, "id": "a1e7119f", "metadata": {}, "outputs": [], "source": [ "# System 1 needs only x and a\n", "def sys1_h(x: float, a: float) -> float:\n", " return a * x\n", "\n", "\n", "# System 2 needs only y and b\n", "def sys2_h(y: float, b: float) -> float:\n", " return b * y\n", "\n", "\n", "system_a = DynamicalSystem(h=sys1_h)\n", "system_b = DynamicalSystem(h=sys2_h)\n", "\n", "# Shared parameter dictionary with all parameters\n", "shared_params = {\n", " \"x\": 2.0,\n", " \"y\": 3.0,\n", " \"a\": 1.5,\n", " \"b\": 2.0,\n", " \"unused_param\": 999, # Ignored by both systems\n", "}\n", "\n", "result_a = system_a.step(params=shared_params)\n", "result_b = system_b.step(params=shared_params)\n", "\n", "print(f\"System A extracts (x, a): {result_a}\")\n", "print(f\"System B extracts (y, b): {result_b}\")" ] }, { "cell_type": "markdown", "id": "83ad806b", "metadata": { "lines_to_next_cell": 0 }, "source": [ ":::{warning}\n", "Names are everything! Be consistent with your naming. Nine times out of ten, when there are issues with this package, it is because the user was not careful with naming their parameters.\n", ":::" ] }, { "cell_type": "markdown", "id": "caadb5d5", "metadata": {}, "source": [ "### Composition\n", "\n", "The real power of the `DynamicalSystem` emerges when we compose multiple systems. See [Example: Car Cruise Control](./car_cruise_control.ipynb)." ] }, { "cell_type": "markdown", "id": "7444f0d8", "metadata": { "lines_to_next_cell": 0 }, "source": [ "### Casting Algorithms in an $(f,h)$-representation\n", "It's one thing to prove that any algorithm has an $(f,h)$-representation--it's another thing to actually create and use such a representation. See [Example: Kalman Filter](../../algorithm_library/kf_pykal.ipynb) for an example on how to do so with a classic state estimation algorithm." ] }, { "cell_type": "markdown", "id": "ff32dbd8", "metadata": {}, "source": [ "[← Modules](../../../getting_started/theory_to_python/modules.rst)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }