{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Rolling Disc From Scratch\n", "\n", "Welcome to this model developers tutorial on creating a rolling disc model from scratch using SymBRiM!\n", "\n", "This notebook is a hands-on tutorial on your own models, connections, and load groups in SymBRiM. By the end of this tutorial, you'll have achieved the following learning goals:\n", "\n", "- Create base classes for models and connections.\n", "- Create a submodel.\n", "- Create a parent model.\n", "- Create a connection.\n", "- Create a load group.\n", "\n", "The setup of this tutorial also aims to follow the style guide used in SymBRiM's source code to acustom you with the style expected by SymBRiM developers. An example is the obligated usage of docstrings and type hints in function signatures.\n", "\n", "Before diving into this tutorial, it's recommended that you familiarize yourself with Objected Oriented Programming (OOP), SymPy, the architecture of SymBRiM (refer to the sections on _Software Overview_ to _BRiM Models_ in [the SymBRiM paper](https://doi.org/10.59490/6504c5a765e8118fc7b106c3)), and the [guidelines on implementing components in SymBRiM](https://mechmotum.github.io/symbrim/guides/component_implementation.html). This background information will provide valuable context for what we'll cover here.\n", "\n", "\n", "## Rolling Disc Model\n", "\n", "Shown below is a freebody diagram of the rolling disc we will model in this tutorial. The rolling disc is defined as an infinitesimally thin disc, rolling on the ground without lateral and longitudinal slip. The disc itself is defined with respect to the ground with a subsequent yaw-roll-pitch body-fixed rotation. Its contact point is defined to be in the ground plane at $q_1 \\hat{n}_x + q_2 \\hat{n}_y$ from the origin. As for the generalized speeds used in Kane's method, those are defined as $u_i = \\dot{q}_i$ for $i=1,...,5$. The ground is treated as the Newtonian body, the disc as a rigid body with a specified mass, inertia, and radius. To control the disc three time-varying torques are used: $T_{drive}$ acts about the rotation axis of the disc, $T_{steer}$ acts about the axis going through the contact point and the center of the disc, and $T_{roll}$ acts about the axis perpendicular to both the normal of the ground and the rotation axis.\n", "\n", "
\"Freebody
\n", "\n", "## Components Overview\n", "\n", "As depicted in the figure below, the rolling disc model (`RollingDisc`) consists of two models, one connection and a set of load groups. Each body is represented by a separate model, as the bodies are expected to be modular. In this case, we will model the ground as a flat ground (`FlatGround`), but one can also use a ground with a slope. The description of a ground in general is defined in the ground base class (`GroundBase`). The wheel model describes the inertial properties and the shape of the disc, in this case a knife-edge wheel (`KnifeEgeWheel`). To describe the interaction between the wheel and the ground, a nonholonomic tire model (`NonHolonomicTire`) is utilized. Reasons for using a connection are that the relation is rather complex to define in the parent model, `RollingDisc`, and it should also be modular and reusable in other parent models. We will use a load group to apply a driving, rolling, and steering torque (`RollingDiscControl`), we will apply gravity after creating the final system instance.\n", "\n", "
\"UML
\n", "\n", "## Tutorial Overview\n", "\n", "This tutorial is structured as follows. We start with an explanation of the implementation of both the abstract base class of the ground model as well as the implementation of the flat ground. Next, we continue with the implementation of the knife-edge wheel, while inheriting the abstract base class, `WheelBase`, from SymBRiM. With both submodels implemented, we will implement the tire base class and tire model, which uses nonholonomic constraints to enforce pure-rolling. Next, we will implement the overarching rolling disc model class. After which we create a load group to control the rolling disc. Finally, we will use our code to form the equations of motion (EoMs) of the rolling disc model.\n", "\n", "Note that the classes implemented in this tutorial are simplified w.r.t. their implemention in the source code of SymBRiM. However, we will try to follow standards used in the development of SymBRiM, like type hinting.\n", "\n", "## Imports\n", "Below we import the classes and functions we will require in this tutorial. Feel free to add any if your missing some." ] }, { "cell_type": "code", "execution_count": 1, "id": "1", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:23.627307Z", "iopub.status.busy": "2024-10-12T22:55:23.626932Z", "iopub.status.idle": "2024-10-12T22:55:24.813880Z", "shell.execute_reply": "2024-10-12T22:55:24.813317Z" } }, "outputs": [], "source": [ "from abc import abstractmethod\n", "\n", "from sympy import Expr, MutableMatrix, Symbol, symbols\n", "from sympy.physics.mechanics import *\n", "from utils import (\n", " check_documentation,\n", " verify_flat_ground,\n", " verify_ground_base,\n", " verify_knife_edge_wheel,\n", " verify_rolling_disc,\n", " verify_rolling_disc_control,\n", " verify_tire_base,\n", ")\n", "\n", "from symbrim.bicycle.wheels import WheelBase\n", "from symbrim.core import (\n", " ConnectionBase,\n", " ConnectionRequirement,\n", " LoadGroupBase,\n", " ModelBase,\n", " ModelRequirement,\n", ")" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "## GroundBase Implementation\n", "\n", "In this section, we will delve into the implementation of the `GroundBase` class, which serves as the base class for defining the properties and methods shared by various ground models in SymBRiM.\n", "\n", "### What is a Base Class?\n", "\n", "A base class, such as `GroundBase`, plays a crucial role in SymBRiM by providing a common structure for related components. In the case of ground models, they share three essential properties and three methods.\n", "\n", "The three attributes defined in `GroundBase` are as follows:\n", "\n", "- `body`: Represents the rigid body associated with the ground.\n", "- `frame`: A reference frame fixed to the ground.\n", "- `origin`: The point that denotes the center of mass of the ground model.\n", "\n", "The three methods prescribed in `GroundBase` are:\n", "\n", "- `normal`: Property returning the normal vector of the ground.\n", "- `tangent_vectors`: Property returning the tangent vectors of the ground plane.\n", "- `set_pos_point(point, position)`: Sets the positions of a point relative to the origin\n", "\n", "These common attributes streamline the creation of different ground models. Subclasses, like the flat ground model we'll discuss, only need to implement specific methods.\n", "\n", "_Note: The actual implementation of the [GroundBase](https://mechmotum.github.io/symbrim/_autosummary/symbrim.bicycle.grounds.GroundBase.html) in SymBRiM is a bit different, as the normal vector and tangent vectors are positional dependent._\n", "\n", "Additionally, `GroundBase` creates a [System](https://docs.sympy.org/dev/modules/physics/mechanics/api/system.html#sympy.physics.mechanics.system.System) instance in the define objects stage, as any model or connection in SymBRiM must do. This [System](https://docs.sympy.org/dev/modules/physics/mechanics/api/system.html#sympy.physics.mechanics.system.System) instance is used to store all information, like bodies, joint, and generalized coordinates, such that these can later be retrieved in the [ModelBase.to_system()](https://mechmotum.github.io/symbrim/_autosummary/symbrim.core.base_classes.ModelBase.html#symbrim.core.base_classes.ModelBase.to_system) method.\n", "\n", "### Define Steps\n", "\n", "In SymBRiM, the definition of a model is divided into several stages, ensuring the proper decoupling between components. These steps are as follows:\n", "\n", "1. **Define connections**: This step enables parent models to associate submodels with their respective connections.\n", "2. **Define objects**: In this step, we create objects, such as symbols and reference frames, without defining any relationships between them.\n", "3. **Define kinematics**: We establish relationships between the objects, specifying their orientations, positions, velocities, and accelerations.\n", "4. **Define loads**: Here, we specify the forces and torques acting upon the system.\n", "5. **Define constraints**: The final step involves computing the holonomic and nonholonomic constraints to which the system is subject.\n", "\n", "To implement the \"define\" steps in a model, connection, or load group, a leading underscore is added to the method name. For example, `_define_`. These methods solely implement the \"define\" step for the component itself without traversing the submodels and load groups.\n", "\n", "[BrimBase](https://mechmotum.github.io/symbrim/_autosummary/symbrim.core.base_classes.BrimBase.html) contains the implementation of the \"define\" methods, including traversal, which should be called by the user. These methods follow the format `define_`.\n", "\n", "For more information refer to [the guidelines on implementing components](https://mechmotum.github.io/symbrim/guides/component_implementation.html#implementation-define-steps).\n", "\n", "### Exercise\n", "\n", "As an exercise, complete the implementation of the `GroundBase` class, seen below, by implementing two specific properties: `origin` and `tangent_vectors`. The `origin` property should return the center of mass of the ground body, and the `tangent_vectors` property should be abstract." ] }, { "cell_type": "code", "execution_count": 2, "id": "3", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.816228Z", "iopub.status.busy": "2024-10-12T22:55:24.815799Z", "iopub.status.idle": "2024-10-12T22:55:24.822611Z", "shell.execute_reply": "2024-10-12T22:55:24.822159Z" } }, "outputs": [], "source": [ "class GroundBase(ModelBase):\n", " \"\"\"Base class for the ground.\"\"\"\n", "\n", " def _define_objects(self) -> None:\n", " \"\"\"Define the objects of the ground.\"\"\"\n", " # When overwriting a method from a parent class, it is good practise to call\n", " # the parent method first. In this case, the _define_objects method of the\n", " # ModelBase class is called.\n", " super()._define_objects()\n", " # Create a rigid body to represent the ground.\n", " self._body = RigidBody(self.name)\n", " self._body.masscenter = Point(self._add_prefix(\"origin\"))\n", " # Create the system object of the ground.\n", " self._system = System.from_newtonian(self.body)\n", "\n", " def _define_kinematics(self) -> None:\n", " \"\"\"Define the kinematics of the ground.\"\"\"\n", " super()._define_kinematics()\n", " # Fixate the origin in the ground frame.\n", " self.origin.set_vel(self.frame, 0)\n", "\n", " @property\n", " def body(self) -> RigidBody:\n", " \"\"\"The body representing the ground.\"\"\"\n", " return self._body\n", "\n", " @property\n", " def frame(self) -> ReferenceFrame:\n", " \"\"\"Frame fixed to the ground.\"\"\"\n", " return self.body.frame\n", "\n", " ### BEGIN SOLUTION\n", " @property\n", " def origin(self) -> Point:\n", " \"\"\"Origin of the ground.\"\"\"\n", " return self.body.masscenter\n", " ### END SOLUTION\n", "\n", " # The abstractmethod decorators make sure that subclasses have to implement these\n", " # methods.\n", " @property\n", " @abstractmethod\n", " def normal(self) -> Vector:\n", " \"\"\"Normal vector of the ground.\"\"\"\n", "\n", " ### BEGIN SOLUTION\n", " @property\n", " @abstractmethod\n", " def tangent_vectors(self) -> tuple[Vector, Vector]:\n", " \"\"\"Tangent vectors of the ground plane.\"\"\"\n", " ### END SOLUTION\n", "\n", " @abstractmethod\n", " def set_pos_point(self, point: Point, position: tuple[Expr, Expr]) -> None:\n", " \"\"\"Locate a point on the ground.\"\"\"\n", "\n", "\n", "# Verification code.\n", "verify_ground_base(GroundBase)" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "## FlatGround Implementation\n", "\n", "In the context of SymBRiM, the flat ground model (`FlatGround`) needs to implement the abstract methods defined in the `GroundBase` class. Specifically, this involves defining the behavior of the ground in terms of the normal vector, tangent vectors, and the ability to set the position of points in the ground plane.\n", "\n", "- The normal vector represents the unit vector in the negative Z-direction, defining the orientation of the ground plane.\n", "- The tangent vectors returns the X- and Y-unit vectors as a tuple, providing a basis for relationships between objects.\n", "- The `set_pos_point` method enables the positioning of a given point (`point`) in the ground plane with respect to the origin.\n", "\n", "### Exercise\n", "\n", "Complete the implementation of the `FlatGround` class below by implementing the following:\n", "\n", "- `tangent_vectors`: Define this property to return the X- and Y-unit vectors as a tuple.\n", "- `set_pos_point`: Implement this method to set the location of a given point (`point`) in the ground plane using the provided position. You can use `Point.set_pos` to set the point's position." ] }, { "cell_type": "code", "execution_count": 3, "id": "5", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.824403Z", "iopub.status.busy": "2024-10-12T22:55:24.824006Z", "iopub.status.idle": "2024-10-12T22:55:24.848418Z", "shell.execute_reply": "2024-10-12T22:55:24.847979Z" } }, "outputs": [], "source": [ "class FlatGround(GroundBase):\n", " \"\"\"Flat ground.\"\"\"\n", "\n", " @property\n", " def normal(self) -> Vector:\n", " \"\"\"Normal vector of the ground.\"\"\"\n", " return -self.frame.z\n", "\n", " ### BEGIN SOLUTION\n", " @property\n", " def tangent_vectors(self) -> tuple[Vector, Vector]:\n", " \"\"\"Tangent vectors of the ground plane.\"\"\"\n", " return self.frame.x, self.frame.y\n", "\n", " def set_pos_point(self, point: Point, position: tuple[Expr, Expr]) -> None:\n", " \"\"\"Set the location of a point on the ground.\"\"\"\n", " point.set_pos(self.origin,\n", " position[0] * self.frame.x + position[1] * self.frame.y)\n", " ### END SOLUTION\n", "\n", "\n", "# Verification code.\n", "verify_flat_ground(FlatGround)" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "## KnifeEdgeWheel Implementation\n", "\n", "In many cases, we don't need to create our own base classes; instead, we can leverage the existing ones provided by SymBRiM. In this section, we will implement a knife-edge wheel, which builds upon the [WheelBase](https://mechmotum.github.io/symbrim/_autosummary/symbrim.bicycle.wheels.WheelBase.html) class from SymBRiM.\n", "\n", "### Inheriting from WheelBase\n", "\n", "Similar to the flat ground, the knife-edge wheel inherits from an abstract class, `WheelBase`. This time we will just use the one defined already in SymBRiM. `WheelBase` already defines a rigid body (`body`) and a body-fixed frame (`frame`) to represent the wheel. Additionally, it prescribes the implementation of two properties:\n", "\n", "- `rotation_axis`: A `Vector` representing the rotation axis.\n", "- `center`: A `Point` representing the wheel's center.\n", "\n", "In the code below, we will implement these two properties. The `center` property returns the center of mass as a `Point` to represent the center of the wheel, and the `rotation_axis` property returns the body-fixed Y-axis as a `Vector` to represent the rotation axis of the wheel.\n", "\n", "### Symbol Handling\n", "\n", "Apart from defining these properties, we need to introduce a `radius` symbol to describe the shape of the wheel. In SymBRiM, we store symbols in the `symbols` dictionary created by [BrimBase](https://mechmotum.github.io/symbrim/_autosummary/symbrim.core.base_classes.BrimBase.html). This dictionary allows users to change a symbol after the \"define object\" stage, providing flexibility in symbol definitions. Here is a short example:\n", "\n", "```python\n", "wheel = KnifeEdgeWheel(\"wheel\")\n", "wheel.define_objects()\n", "print(repr(wheel.symbols[\"r\"])) # Prints Symbol(\"wheel_r\")\n", "wheel.symbols[\"r\"] = Symbol(\"my_r\")\n", "print(repr(wheel.symbols[\"r\"])) # Prints Symbol(\"my_r\")\n", "```\n", "\n", "Additionally, descriptions for symbols are stored in the `descriptions` property, which can be used for documentation purposes. `BrimBase.get_description` uses these properties to allow a user to request the definition of a symbol, e.g. `bicycle.get_description(wheel.symbols[\"r\"])` should return something like `Radius of the wheel.`.\n", "\n", "Within SymBRiM's source code we use `BrimBase._add_prefix` to add the name of the instantiated model as a prefix to each symbol. This ensures that the symbols created among different models stay unique, assuming the user instantiates the models with unique names. For example:\n", "\n", "```python\n", "wheel_rear = KnifeEdgeWheel(\"wheel_rear\")\n", "wheel_front = KnifeEdgeWheel(\"wheel_front\")\n", "wheel_rear.define_all()\n", "wheel_front.define_all()\n", "assert wheel_rear.symbols[\"r\"] != wheel_front.symbols[\"r\"]\n", "```\n", "\n", "For more information refer to [the guidelines on implementing components](https://mechmotum.github.io/symbrim/guides/component_implementation.html#define-objects).\n", "\n", "### Exercise\n", "\n", "Complete the implementation of the `KnifeEdgeWheel` class by implementing the following:\n", "\n", "- `center`: Define this property, which returns the center of mass as a `Point` to represent the center of the wheel.\n", "- `rotation_axis`: Define this property, which returns the body-fixed Y-axis as a `Vector` to represent the rotation axis of the wheel.\n", "- Define a symbol for the radius and use ``\"r\"`` as key in the ``self.symbols`` dictionary." ] }, { "cell_type": "code", "execution_count": 4, "id": "7", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.850075Z", "iopub.status.busy": "2024-10-12T22:55:24.849914Z", "iopub.status.idle": "2024-10-12T22:55:24.895413Z", "shell.execute_reply": "2024-10-12T22:55:24.894904Z" } }, "outputs": [], "source": [ "class KnifeEdgeWheel(WheelBase):\n", " \"\"\"Knife-edge wheel.\"\"\"\n", "\n", " ### BEGIN SOLUTION\n", " @property\n", " def descriptions(self) -> dict[object, str]:\n", " \"\"\"Descriptions of the attributes of the wheel.\"\"\"\n", " return {\n", " **super().descriptions,\n", " self.symbols[\"r\"]: \"\"\"Radius of the wheel.\"\"\",\n", " }\n", "\n", " def _define_objects(self) -> None:\n", " \"\"\"Define the objects of the wheel.\"\"\"\n", " super()._define_objects()\n", " self.symbols[\"r\"] = Symbol(self._add_prefix(\"r\"))\n", "\n", " @property\n", " def center(self) -> Point:\n", " \"\"\"Point representing the center of the wheel.\"\"\"\n", " return self.body.masscenter\n", "\n", " @property\n", " def rotation_axis(self) -> Vector:\n", " \"\"\"Rotation axis of the wheel.\"\"\"\n", " return self.frame.y\n", " ### END SOLUTION\n", "\n", "\n", "# Verification code.\n", "verify_knife_edge_wheel(KnifeEdgeWheel)" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "## TireBase Implementation\n", "\n", "In the context of the rolling disc model, the interaction between the wheel and the ground is complex and demands a modular design. To accommodate this, a new type of connection has been introduced to specify the tire model responsible for defining this interaction. This connection is facilitated by the abstract class `TireBase`.\n", "\n", "_Note: The implementation of tire model connections can be challenging, so if you find the details daunting, you can continue reading and return later._\n", "\n", "### Functionality Overview\n", "\n", "The `TireBase` class provides several essential properties and methods:\n", "\n", "- `ground`: A property that accepts any type of ground model, as long as it inherits from `GroundBase`.\n", "- `wheel`: A property that accepts any type of wheel model, as long as it inherits from `WheelBase`.\n", "- `contact_point`: A property that returns a `Point` to represent the contact point between the wheel and the ground.\n", "- `on_ground`: A boolean property that specifies whether the wheel is defined to always touch the ground or whether a holonomic constraint should be used. This boolean is by default `False`, but can be set to `True` by the parent.\n", "\n", "All other functionalities, such as computing the constraints and tire forces, are meant to be implemented in the define steps. For instance, the nonholonomic constraints should be set in `define_constraints` when implementing `NonHolonomicTire`.\n", "\n", "### Creation of Submodel Properties\n", "\n", "In SymBRiM, the class property `required_models` is used to specify submodel properties. `required_models` should be a tuple of `ModelRequirements`. Refer to [the implementation guidelines](https://mechmotum.github.io/symbrim/guides/component_implementation.html#setting-submodels-and-connections) for more information.\n", "\n", "### Contact Point Computation\n", "\n", "Since most tire models require computing the contact point between the wheel and the ground, `TireBase` also implements `_set_pos_contact_point`. This protected method computes the contact point based on the types and properties of the ground and wheel. Once computed, it saves the location w.r.t. the wheel. It's important to note that this method should be called by the child class, e.g., `NonHolonomicTire`, as not every tire model may utilize this method.\n", "\n", "### Exercise\n", "\n", "Implement the following methods/properties in the `TireBase` class. Refer above for details on each of the properties.\n", "\n", "- Implement the `ground` and `wheel` properties using the `required_models` class attribute.\n", "- Do not forget to instantiate a `System` in `_define_objects`.\n", "- Implement the `on_ground` property, while setting it to `False` by default in `_define_objects`.\n", "- Implement the `contact_point` property, while instantiating the point in `_define_objects`.\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "9", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.897235Z", "iopub.status.busy": "2024-10-12T22:55:24.896927Z", "iopub.status.idle": "2024-10-12T22:55:24.923572Z", "shell.execute_reply": "2024-10-12T22:55:24.923118Z" } }, "outputs": [], "source": [ "class TireBase(ConnectionBase):\n", " \"\"\"Base class for the tire model connectors.\"\"\"\n", "\n", " ### BEGIN SOLUTION\n", " required_models: tuple[ModelRequirement, ...] = (\n", " ModelRequirement(\"ground\", GroundBase, \"Submodel of the ground.\"),\n", " ModelRequirement(\"wheel\", WheelBase, \"Submodel of the wheel.\"),\n", " )\n", " # These type hints are useful for IDEs as the properties are created dynamically.\n", " ground: GroundBase\n", " wheel: WheelBase\n", " ### END SOLUTION\n", "\n", " def _set_pos_contact_point(self) -> None:\n", " \"\"\"Compute the contact point of the wheel with the ground.\"\"\"\n", " if (\n", " isinstance(self.ground, FlatGround)\n", " and isinstance(self.wheel, KnifeEdgeWheel)\n", " ):\n", " self.wheel.center.set_pos(\n", " self.contact_point,\n", " self.wheel.symbols[\"r\"] * cross(\n", " self.wheel.rotation_axis,\n", " cross(self.ground.normal, self.wheel.rotation_axis)).normalize()\n", " )\n", " return\n", " raise NotImplementedError(\n", " f\"Computation of the contact point has not been implemented for the \"\n", " f\"combination of {type(self.ground)} and {type(self.wheel)}.\")\n", "\n", " ### BEGIN SOLUTION\n", " def _define_objects(self) -> None:\n", " \"\"\"Define the objects of the tire model.\"\"\"\n", " super()._define_objects()\n", " self._system = System.from_newtonian(self.ground.body)\n", " self._contact_point = Point(self._add_prefix(\"contact_point\"))\n", " self._on_ground = False\n", "\n", " @property\n", " def contact_point(self) -> Point:\n", " \"\"\"Point representing the contact point of the wheel with the ground.\"\"\"\n", " return self._contact_point\n", "\n", " @property\n", " def on_ground(self) -> bool:\n", " \"\"\"Boolean whether the wheel is already defined as touching the ground.\"\"\"\n", " return self._on_ground\n", "\n", " @on_ground.setter\n", " def on_ground(self, value: bool) -> None:\n", " self._on_ground = bool(value)\n", " ### END SOLUTION\n", "\n", "\n", "# Verification code.\n", "verify_tire_base(TireBase, FlatGround, KnifeEdgeWheel)" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "## NonHolonomicTire Implementation\n", "\n", "The `NonHolonomicTire` is a tire connection that enforces pure-rolling using nonholonomic constraints. It inherits from `TireBase` and re-specifies the class attribute `required_models`, as it will only be working with the `KnifeEdgeWheel` and `FlatGround`.\n", "\n", "During the define kinematics step, the `_set_pos_contact_point` method is employed to determine the position of the contact point. The nonholonomic constraints are defined in the `_define_constraints` method. Two distinct constructs are employed to calculate the velocity of the wheel center. Both of these velocities should be the same. However, they use different mathematical equations. The constructs use are:\n", "\n", "1. The velocity of the wheel center is calculated by taking the time derivative of its position.\n", "\n", "2. The contact point serves as the instantaneous center of rotation. By leveraging this fact, the velocity can be computed as the cross product of the angular velocity of the wheel and the distance from the contact point to the wheel's center." ] }, { "cell_type": "code", "execution_count": 6, "id": "11", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.925308Z", "iopub.status.busy": "2024-10-12T22:55:24.925004Z", "iopub.status.idle": "2024-10-12T22:55:24.929564Z", "shell.execute_reply": "2024-10-12T22:55:24.929054Z" } }, "outputs": [], "source": [ "class NonHolonomicTire(TireBase):\n", " \"\"\"Tire model connection based on nonholonomic constraints.\"\"\"\n", "\n", " required_models: tuple[ModelRequirement, ...] = (\n", " ModelRequirement(\"ground\", FlatGround, \"Submodel of the ground.\"),\n", " ModelRequirement(\"wheel\", KnifeEdgeWheel, \"Submodel of the wheel.\"),\n", " )\n", "\n", " def _define_kinematics(self) -> None:\n", " \"\"\"Define the kinematics of the tire model.\"\"\"\n", " super()._define_kinematics()\n", " self._set_pos_contact_point()\n", "\n", " def _define_constraints(self) -> None:\n", " \"\"\"Define the constraints of the tire model.\"\"\"\n", " super()._define_constraints()\n", " # Get the normal and tangent vectors of the ground at the contact point.\n", " normal = self.ground.normal\n", " tangent_vectors = self.ground.tangent_vectors\n", " # Compute the velocity of wheel center using two different constructs.\n", " v1 = self.wheel.center.pos_from(self.ground.origin).dt(self.ground.frame)\n", " v2 = cross(self.wheel.frame.ang_vel_in(self.ground.frame),\n", " self.wheel.center.pos_from(self.contact_point))\n", " # Compute and add the nonholonomic constraints.\n", " self.system.add_nonholonomic_constraints(\n", " dot(v1 - v2, tangent_vectors[0]), dot(v1 - v2, tangent_vectors[1]))\n", " # Add a holonomic constraint if the wheel is not defined to be on the ground.\n", " if not self.on_ground:\n", " self.system.add_holonomic_constraints(\n", " self.contact_point.pos_from(self.ground.origin).dot(normal))" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "## Rolling Disc Model Implementation\n", "\n", "Now that we have all are key components implemted, we can create the `RollingDisc` model. While each of the components defines its own system, the job of the `RollingDisc` as parent model is to connect them. It has two submodels, ground and wheel, which it needs to connect. In this definition it should use our newly created tire connection as utility.\n", "\n", "The first step of implementing the `RollingDisc` class is to specify its submodels. Like with the connections, we can do so by specifying the class property `required_models` with `ModelRequirement`s. Required connections can be specified similarly using the class property `required_connections` with `ConnectionRequirement`s.\n", "\n", "With the structure set up we need to implement each of the define steps according to [the guidelines](https://mechmotum.github.io/brim/guides/component_implementation.html#implementation-define-steps). In define connections we must associate the `wheel` and `ground` with the `tire` connection. \n", "\n", "The following objects should be defined in the define objects step:\n", "\n", "- A `System` should be instantiated (as always).\n", "- The define step for each connection should be called.\n", "- The `on_ground` property of the tire should be set to `True` because we will define the contact point to be in the ground plane by definition not with a holonomic constraint.\n", "- Generalized coordinates and speeds must be created. The `RollingDisc` will use these to orient and position the wheel w.r.t. the ground. For each of these we should of course also make a description.\n", "\n", "In the define kinematics step `RollingDisc` has to define the wheel w.r.t. the ground. The orientation can be done using [ReferenceFrame.orient_body_fixed](https://docs.sympy.org/dev/modules/physics/vector/api/classes.html#sympy.physics.vector.frame.ReferenceFrame.orient_body_fixed), while setting the position of the contact point using `GroundBase._set_pos_point`. In this process it should also utilize the tire connection to establish some of the relationships. For details on the kinematics refer to the free body diagram at the top of this page. _Hint: do not forget to add the generalized coordinates, generalized speeds, and kinematical differential equations to the system._\n", "\n", "As for the define loads and define constraints step, the only necessity is to call the define step for the connections, in case those would define any loads or constraints.\n", "\n", "### Exercise\n", "\n", "Implement the `RollingDisc` model using the components you have just implemented." ] }, { "cell_type": "code", "execution_count": 7, "id": "13", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:24.931363Z", "iopub.status.busy": "2024-10-12T22:55:24.931020Z", "iopub.status.idle": "2024-10-12T22:55:25.090277Z", "shell.execute_reply": "2024-10-12T22:55:25.089740Z" } }, "outputs": [], "source": [ "class RollingDisc(ModelBase):\n", " \"\"\"Rolling disc model.\"\"\"\n", "\n", " ### BEGIN SOLUTION\n", " required_models: tuple[ModelRequirement, ...] = (\n", " ModelRequirement(\"ground\", GroundBase, \"Ground model.\"),\n", " ModelRequirement(\"wheel\", WheelBase, \"Wheel model.\"),\n", " )\n", " required_connections: tuple[ConnectionRequirement, ...] = (\n", " ConnectionRequirement(\"tire\", TireBase, \"Tire model.\"),\n", " )\n", "\n", " @property\n", " def descriptions(self) -> dict[object, str]:\n", " \"\"\"Dictionary of descriptions of the rolling disc's attributes.\"\"\"\n", " desc = {\n", " **super().descriptions,\n", " self.q[0]: \"Perpendicular distance along ground.x to the contact point.\",\n", " self.q[1]: \"Perpendicular distance along ground.y to the contact point.\",\n", " self.q[2]: \"Yaw angle of the disc.\",\n", " self.q[3]: \"Roll angle of the disc.\",\n", " self.q[4]: \"Pitch angle of the disc.\",\n", " }\n", " desc.update({ui: f\"Generalized speed of the {desc[qi].lower()}\"\n", " for qi, ui in zip(self.q, self.u)})\n", " return desc\n", "\n", " def _define_connections(self) -> None:\n", " \"\"\"Define the connections between the submodels.\"\"\"\n", " super()._define_connections()\n", " self.tire.ground = self.ground\n", " self.tire.wheel = self.wheel\n", "\n", " def _define_objects(self) -> None:\n", " \"\"\"Define the objects of the rolling disc.\"\"\"\n", " super()._define_objects()\n", " # Define the system instance with the same reference as the ground.\n", " self._system = System(self.ground.frame, self.ground.origin)\n", " # Setup the tire model.\n", " self.tire.define_objects()\n", " self.tire.on_ground = True\n", " # Define the generalized coordinates and speeds.\n", " self.q = MutableMatrix([dynamicsymbols(self._add_prefix(\"q1:6\"))])\n", " self.u = MutableMatrix([dynamicsymbols(self._add_prefix(\"u1:6\"))])\n", "\n", " def _define_kinematics(self) -> None:\n", " \"\"\"Define the kinematics of the rolling disc.\"\"\"\n", " super()._define_kinematics()\n", " # Define the yaw-roll-pitch orientation of the disc.\n", " self.wheel.frame.orient_body_fixed(self.ground.frame, self.q[2:], \"zxy\")\n", " # Define the position of the contact point in the ground plane.\n", " self.ground.set_pos_point(self.tire.contact_point, self.q[:2])\n", " # Define the kinematics of the tire model.\n", " self.tire.define_kinematics()\n", " # Add coordinates, speeds and kinematic differential equations to the system.\n", " self.system.add_coordinates(*self.q)\n", " self.system.add_speeds(*self.u)\n", " self.system.add_kdes(*(self.q.diff(dynamicsymbols._t) - self.u))\n", "\n", " def _define_loads(self) -> None:\n", " \"\"\"Define the loads of the rolling disc.\"\"\"\n", " super()._define_loads()\n", " self.tire.define_loads()\n", "\n", " def _define_constraints(self) -> None:\n", " \"\"\"Define the constraints of the rolling disc.\"\"\"\n", " super()._define_constraints()\n", " self.tire.define_constraints()\n", " ### END SOLUTION\n", "\n", "# Verification code.\n", "verify_rolling_disc(RollingDisc, FlatGround, KnifeEdgeWheel, NonHolonomicTire)" ] }, { "cell_type": "markdown", "id": "14", "metadata": {}, "source": [ "## Control Load Group Implementation\n", "\n", "We of course want to also control the disc using some loads, namely a drive, roll and steer torque at the wheel. To easily allow users to do so we can create a load group `RollingDiscControl`. When creating a load group the first step is to set the required type of the parent. In this case, the rolling disc is chosen as parent. This can be done by specifying the class property `required_parent_type`.\n", "\n", "### Exercise\n", "\n", "Complete the implementation of `RollingDiscControl`, which applies a drive, roll and steer torque a the wheel. Each of the magnitudes should be defined as a dynamicsymbol and be stored in the `self.symbols` dictionary under the names `\"T_drive\"`, `\"T_roll\"`, and `\"T_steer\"` respectively. You can compute the roll axis using `cross(self.parent.ground.normal, self.parent.wheel.rotation_axis)` and the upward radial axis using `cross(self.parent.wheel.rotation_axis, roll_axis)`." ] }, { "cell_type": "code", "execution_count": 8, "id": "15", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:25.092317Z", "iopub.status.busy": "2024-10-12T22:55:25.092150Z", "iopub.status.idle": "2024-10-12T22:55:25.142052Z", "shell.execute_reply": "2024-10-12T22:55:25.141555Z" } }, "outputs": [], "source": [ "class RollingDiscControl(LoadGroupBase):\n", " \"\"\"Rolling disc control load group.\"\"\"\n", "\n", " ### BEGIN SOLUTION\n", " required_parent_type = RollingDisc\n", "\n", " @property\n", " def descriptions(self) -> dict[object, str]:\n", " \"\"\"Dictionary of descriptions of the rolling disc's controls.\"\"\"\n", " return {\n", " **super().descriptions,\n", " self.symbols[\"T_drive\"]: \"Drive torque.\",\n", " self.symbols[\"T_roll\"]: \"Roll torque.\",\n", " self.symbols[\"T_steer\"]: \"Steer torque.\",\n", " }\n", "\n", " def _define_objects(self) -> None:\n", " \"\"\"Define objects.\"\"\"\n", " super()._define_objects()\n", " self.symbols.update({\n", " name: dynamicsymbols(self._add_prefix(name)) for name in (\n", " \"T_drive\", \"T_roll\", \"T_steer\")\n", " })\n", "\n", " def _define_loads(self) -> None:\n", " \"\"\"Define loads.\"\"\"\n", " super()._define_loads()\n", " roll_axis = cross(self.parent.ground.normal, self.parent.wheel.rotation_axis)\n", " upward_radial_axis = cross(self.parent.wheel.rotation_axis, roll_axis)\n", " self.system.add_loads(\n", " Torque(self.parent.wheel.frame,\n", " self.symbols[\"T_drive\"] * self.parent.wheel.rotation_axis +\n", " self.symbols[\"T_roll\"] * roll_axis +\n", " self.symbols[\"T_steer\"] * upward_radial_axis,\n", " )\n", " )\n", " ### END SOLUTION\n", "\n", "# Verification code.\n", "verify_rolling_disc_control(RollingDiscControl, RollingDisc, FlatGround, KnifeEdgeWheel, NonHolonomicTire)" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "## Build Rolling Disc\n", "\n", "With all components defined the rolling disc model we can form the EoMs of our rolling disc. We should start with configuring the model by describing out of which components the rolling disc is composed. Next, we have to run all define steps. After which we can export it to a single system instance and apply gravity. The last step before generating the EoMs is to specify which generalized speeds are independent and which are dependent.\n", "\n", "### Exercise\n", "\n", "Build the your rolling disc model and form the EoMs. You can use `System.validate_system()` to do some simple checks. Also, if you would like to use your equations, make sure to specify `\"CRAMER\"` as constraint solver." ] }, { "cell_type": "code", "execution_count": 9, "id": "17", "metadata": { "execution": { "iopub.execute_input": "2024-10-12T22:55:25.144165Z", "iopub.status.busy": "2024-10-12T22:55:25.143803Z", "iopub.status.idle": "2024-10-12T22:55:25.828317Z", "shell.execute_reply": "2024-10-12T22:55:25.827807Z" } }, "outputs": [], "source": [ "### BEGIN SOLUTION\n", "# Configure the model by describing the components it consists out of.\n", "rolling_disc = RollingDisc(\"disc\")\n", "rolling_disc.wheel = KnifeEdgeWheel(\"wheel\")\n", "rolling_disc.ground = FlatGround(\"ground\")\n", "rolling_disc.tire = NonHolonomicTire(\"tyre\")\n", "rolling_disc.add_load_groups(RollingDiscControl(\"controls\"))\n", "# Run all define steps.\n", "rolling_disc.define_all()\n", "# Export the model to a single instance of System.\n", "system = rolling_disc.to_system()\n", "# Apply gravity.\n", "system.apply_uniform_gravity(-Symbol(\"g\") * rolling_disc.ground.normal)\n", "# Define which generalized speeds are indepedent and dependent\n", "system.u_ind = rolling_disc.u[2:]\n", "system.u_dep = rolling_disc.u[:2]\n", "# Run some basic validation of the system before forming the EoMs.\n", "system.validate_system()\n", "system.form_eoms();\n", "### END SOLUTION" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "## What's Next?\n", "\n", "Congratulations! You have finished the developers tutorial. Now that you have completed this tutorial you can start developing your own models. If you would like to practise further, here are some ideas:\n", "\n", "- Implement your own `UniCycle` model. Feel free to open a PR ;)\n", "- Try to reduce the number of operations in the EoMs after CSE of your `RollingDisc`. You can compute the number of operations using `sympy.cse(system.form_eoms(constraint_solver=\"CRAMER\"))`.\n", "- Read the implementation of the models you have implemented in BRiM's source code. There will most probably be some subtle differences." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.10" } }, "nbformat": 4, "nbformat_minor": 5 }