{ "cells": [ { "cell_type": "markdown", "id": "75a5ba2d-66ec-4e85-9d4a-fe1ee7d24d57", "metadata": {}, "source": [ "© Copyright, 2025 G. Schaer.\n", "\n", "SPDX-License-Identifier: GPL-3.0-only" ] }, { "cell_type": "markdown", "id": "9bd6dd9a-a0f6-4248-89d4-657c318e0d79", "metadata": {}, "source": [ "# Tutorial 0: Introduction" ] }, { "cell_type": "markdown", "id": "3c18bd9e-7470-4b09-b7a7-fac8bb2dec17", "metadata": {}, "source": [ "## Tutorial Description" ] }, { "cell_type": "markdown", "id": "b7e48aef-41be-4597-be37-1bd196bc97a5", "metadata": {}, "source": [ "This tutorial covers creating a new `Project` using `condynsate`. In this tutorial, we will cover how to: \n", "1. Load objects in a `condynsate` `Project`.\n", "2. Test that objects behave as expected.\n", " \n", "We will accomplish these goals by loading a cube 1 meter above a solid ground plane, starting the physics engine, then observing the dynamics of the cube. Note that this project will not coverhow to create your own .urdf files. Instead, we recommend reviewing https://wiki.ros.org/urdf." ] }, { "cell_type": "markdown", "id": "be64a904-72de-4411-8a77-37a630f0c3a4", "metadata": {}, "source": [ "## Imports" ] }, { "cell_type": "markdown", "id": "5ca5c838-ffae-45eb-9e60-0ad1e0ac8698", "metadata": {}, "source": [ "To begin, we import the required dependencies. For most applications, the `condynsate.Project` class, is used. The `Project` class automatically combines and managers the `condynsate` simulation engine, visualization engine, animation engine, and keyboard listener. \n", "\n", "We also import `condynsate.__assets__`. `__assets__` is a dictionary that defines the path to all default assets that come with `condynsate` including .urdf files, .stl files, and .png texture files. In thic case, we use `__assets__` to load the default cube and the default ground plane." ] }, { "cell_type": "code", "execution_count": 1, "id": "b2b6dc1e-8420-4242-91a3-e73422ce713e", "metadata": {}, "outputs": [], "source": [ "from condynsate import Project\n", "from condynsate import __assets__ as assets" ] }, { "cell_type": "markdown", "id": "61754c2f-38bd-4bed-8c3b-691c24fd175e", "metadata": {}, "source": [ "To see what default assets are available to us, we can list the keys of `assets`:" ] }, { "cell_type": "code", "execution_count": 2, "id": "2683e90e-88af-4340-92ab-65f901732817", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dict_keys(['arrow_ccw.stl', 'arrow_lin.stl', 'bricks.png', 'carpet.png', 'cart.urdf', 'classroom_wall.png', 'concrete.png', 'cone_1x1_center_origin.stl', 'cone_1x1_lower_origin.stl', 'cube.urdf', 'cube_1x1x1_center_origin.stl', 'cube_1x1x1_lower_origin.stl', 'cylinder_1x1_center_origin.stl', 'cylinder_1x1_lower_origin.stl', 'door_wall.png', 'gyro.urdf', 'gyrocore.stl', 'gyroouter_1-odx0.9id_lower_origin.stl', 'gyroring_0.6-odx0.3-id_center_origin.stl', 'gyroring_0.9-odx0.6-id_center_origin.stl', 'half_plane_medium.urdf', 'missing_tex.png', 'plane.obj', 'plane_big.urdf', 'plane_medium.urdf', 'plane_small.urdf', 'pyramid_1x1x1_center_origin.stl', 'pyramid_1x1x1_lower_origin.stl', 'sphere_1_center_origin.stl', 'sphere_1_lower_origin.stl', 'white_wall.png', 'window_wall.png'])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "assets.keys()" ] }, { "cell_type": "markdown", "id": "dea83d70-5877-4057-b588-983d28579958", "metadata": {}, "source": [ "## Initializing the Project Class" ] }, { "cell_type": "markdown", "id": "2f7377b9-1114-4fb5-8e61-29571cae9486", "metadata": {}, "source": [ "To begin, we initialize a member of the `Project` class. Doing so will automatically initialize a simulation engine and open the visualization engine.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.Project |\n", " -----------------------------------------------------------------------------\n", " The Project class ties together a Simulator, Visualizer, Animator, and\n", " Keyboard into class.\n", "\n", " Parameters\n", " ----------\n", " **kwargs\n", "\n", " Keyword Args\n", " ------------\n", " simulator_gravity : 3 tuple of floats, optional\n", " The gravity vector used in the simulation. The default value is\n", " (0.0, 0.0, -9.81).\n", " simulator_dt : float, optional\n", " The finite time step size used by the simulator. If set too\n", " small, can result in visualizer, simulator desynch. Too small\n", " is determined by the number of total links in the simulation.\n", " The default value is 0.01.\n", " visualizer : bool, optional\n", " A boolean flag that indicates if the project should include a\n", " visualizer. This visualizer provides a 3D rendering of the\n", " simulation state. The default is True.\n", " visualizer_frame_rate : bool, optional\n", " The frame rate of the visualizer. When None, attempts to run at\n", " unlimited. This is not reccomended because it can cause\n", " communication bottlenecks that cause slow downs. The default\n", " value is 45.\n", " visualizer_record : bool, optional\n", " A boolean flag that indicates if the visualizer will record.\n", " True, all frames from the start function call to the terminate\n", " function call are recorded. After the terminate function call,\n", " these frames are saved with h.264 and outputs in an MP4\n", " container. The saved file name has the form visualizer.mp4.\n", " The default is False.\n", " animator : bool, optional\n", " A boolean flag that indicates if the project should include an\n", " animator. This animator provides real-time 2D plotting.\n", " The default is False.\n", " animator_frame_rate : float, optional\n", " The upper limit of the allowed frame rate in frames per second.\n", " When set, the animator will not update faster than this speed.\n", " The default is 15.0\n", " animator_record : bool, optional\n", " A boolean flag that indicates if the animator should be\n", " recorded. If True, all frames from the start function call to\n", " the terminate function call are recorded. After the terminate\n", " function call, these frames are saved with h.264 and outputs in\n", " an MP4 container. The saved file name has the form\n", " animator.mp4. The default is False.\n", "\n", " Attributes\n", " ----------\n", " simulator : condynsate.Simulator\n", " The instance of the condynsate.Simulator class used by this project.\n", " visualizer : condynsate.Visualizer or None\n", " The instance of the condynsate.Visualizer class used by this project.\n", " None if this project uses no visualizer.\n", " animator : condynsate.Animator or None\n", " The instance of the condynsate.Animator class used by this project.\n", " None if this project uses no animator.\n", " keyboard : condynsate.Keyboard or None\n", " The instance of the condynsate.Keyboard class used by this project.\n", " None if this project uses no keyboard.\n", " bodies : List of condynsate.simulator.objects.Body\n", " All bodies loaded into the project via the load_urdf fnc.\n", " simtime : float\n", " The current simulation time in seconds." ] }, { "cell_type": "code", "execution_count": 3, "id": "d493c716-fd12-4537-8836-ebc787cdba99", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "You can open the visualizer by visiting the following URL:\n", "http://127.0.0.1:7027/static/\n" ] } ], "source": [ "# Create the project\n", "proj = Project()" ] }, { "cell_type": "markdown", "id": "9dc52619-b89b-4c83-a9bb-8f7b8ec3a3cc", "metadata": {}, "source": [ "You should now see the default empty environment opened in your default web browser." ] }, { "cell_type": "markdown", "id": "aaa50320-5117-43a6-844d-5cc4d9180500", "metadata": {}, "source": [ "We are now able to interact with the `Visualizer`. For now, let's turn off the grid plane and axes. These are both visual references and not physical objects." ] }, { "cell_type": "code", "execution_count": 4, "id": "39bacc3c-76c1-4c84-b5d2-f7d853dd8d2a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "proj.visualizer.set_axes(False) # Returns 0 on success\n", "proj.visualizer.set_grid(False) # Returns 0 on success" ] }, { "cell_type": "markdown", "id": "86962649-ceee-40af-8a1b-337d2c7dcc36", "metadata": {}, "source": [ "Next, we will load our first physics object, the ground plane. To load physics objects we use the `condynsate.Project.load_urdf` function:\n", " \n", " -----------------------------------------------------------------------------\n", " | condynsate.Project.load_urdf |\n", " -----------------------------------------------------------------------------\n", " Loads a body defined by a .URDF file (https://wiki.ros.org/urdf) into \n", " the project. \n", " \n", " Parameters \n", " ---------- \n", " path : string \n", " The path pointing to the .URDF file that defines the body. \n", " **kwargs \n", " \n", " Keyword Args \n", " ------------ \n", " fixed : boolean, optional \n", " A flag that indicates if the body is fixed (has 0 DoF) or free \n", " (has 6 DoF). \n", " \n", " Returns \n", " ------- \n", " body : condynsate.core.objects.Body \n", " The body added to the simulation. This retured object facilitates \n", " user interaction with the body and its joints and links. \n" ] }, { "cell_type": "code", "execution_count": 5, "id": "573152b4-5b78-4aec-b3d9-f409e0a1ce0d", "metadata": {}, "outputs": [], "source": [ "# Load a fixed plane as the ground object\n", "ground = proj.load_urdf(assets['plane_medium.urdf'], fixed=True)" ] }, { "cell_type": "markdown", "id": "8906ba6c-a290-4aab-bff7-a243857fa8a9", "metadata": {}, "source": [ "Once the ground plane is loaded, we will add a cube and set its initial state to 1 meter above the ground. To se the initial state, we use the \n", "`condynsate.simulator.objects.Body.set_initial_state` function.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.simulator.objects.Body.set_initial_state |\n", " -----------------------------------------------------------------------------\n", " Sets the initial state of the body. When the simulation is\n", " reset, this object will be reset to this state.\n", "\n", " Parameters\n", " ----------\n", " **kwargs\n", "\n", " Keyword Args\n", " ------------\n", " position : 3 tuple of floats, optional\n", " The XYZ position in world coordinates. The default is (0., 0., 0.)\n", " yaw : float, optional\n", " The (z-y'-x' Tait–Bryan) yaw angle of the object in radians.\n", " pitch : float, optional\n", " The (z-y'-x' Tait–Bryan) pitch angle of the object in radians.\n", " roll : float, optional\n", " The (z-y'-x' Tait–Bryan) roll angle of the object in radians.\n", " velocity : 3 tuple of floats, optional\n", " The XYZ velocity in either world or body coordinates. Body\n", " coordinates are defined based on object's orientation.\n", " The default is (0., 0., 0.)\n", " omega : 3 tuple of floats, optional\n", " The XYZ angular velocity in either world or body coordinates.\n", " Body coordinates are defined based on object's orientation.\n", " The default is (0., 0., 0.)\n", " body : bool, optional\n", " Whether velocity and omega are being set in world or body\n", " coordinates. The default is False\n", "\n", " Returns\n", " -------\n", " ret_code : int\n", " 0 if successful, -1 if something went wrong." ] }, { "cell_type": "code", "execution_count": 6, "id": "129a9625-b65e-42ab-b735-cc38877fe920", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Load a cube object and set its initial position to 1 meter above the ground\n", "cube = proj.load_urdf(assets['cube.urdf'])\n", "\n", "# The origin of the cube is at its center so\n", "# Setting its initial state to (0,0,1.5) places the bottom face 1 meter above the ground\n", "cube.set_initial_state(position=(0,0,1.5)) # Returns 0 on success" ] }, { "cell_type": "markdown", "id": "f077dbda-9877-4bdc-a38c-f53434cc9046", "metadata": {}, "source": [ "Notice that upon setting the initial state, the `Visualizer` is not updated automatically. Where as calls to `Project` member functions will automatically update the `Visualizer`, calls to other classes' member functions will not. To reflect our changes, we can now call the `condynsate.Project.refresh_visualizer` function.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.Project.refresh_visualizer |\n", " -----------------------------------------------------------------------------\n", " Refreshes the visualizer to synchronize it to the current simulator\n", " state. This is automatically called by load_urdf, reset, step,\n", " await_keypress, and await_anykeys. Therefore, its use cases are limited\n", " to when bodies are modified outside of the main simulation loop.\n", "\n", " Returns\n", " -------\n", " ret_code : int\n", " 0 if successful, -1 if something went wrong." ] }, { "cell_type": "code", "execution_count": 7, "id": "0bbb5dc3-9cbe-472e-a266-f9bf0e28880e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "proj.refresh_visualizer() # Returns 0 on success" ] }, { "cell_type": "markdown", "id": "420433c6-c1d2-4702-aba4-52295bf753e2", "metadata": {}, "source": [ "## Running a Simulation Loop" ] }, { "cell_type": "markdown", "id": "3e568e60-794e-4cc0-93db-0349dda0336c", "metadata": {}, "source": [ "Before running a simulation, it is important to call the `condynsate.Project.reset` function. This function resets the `Simulator`, `Visualizer`, and, if applicable, `Animator` to the initial state and synchronizes them.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.Project.reset |\n", " -----------------------------------------------------------------------------\n", " Resets the simulator, visualizer (if there is one), and animator\n", " (if there is one). If the animator is not already running, starts the\n", " animator then resets it\n", "\n", " Returns\n", " -------\n", " ret_code : int\n", " 0 if successful, -1 if something went wrong." ] }, { "cell_type": "code", "execution_count": 8, "id": "85dd5c3a-760b-4c6b-ab5c-bfdbba345f79", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "proj.reset() # Returns 0 on success" ] }, { "cell_type": "markdown", "id": "8ede45c8-83c4-44f3-b57d-35c52d8c835e", "metadata": {}, "source": [ "Now everything is set up and we are ready to run a simulation loop. To do so, we use the `condynsate.Project.step` function. This function takes a single physics step and updates the `Visualizer` to reflect changes in the `Simulator` state. We will call the `step` function in a loop until the simulation time is 2.0 seconds. Doing so will run a 2.0 second simulation.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.Project.step |\n", " -----------------------------------------------------------------------------\n", " Takes a single simulation step and updates the visualizer to reflect\n", " the new simulator state.\n", "\n", " Parameters\n", " ----------\n", " real_time : bool, optional\n", " A boolean flag that indicates whether the step is to be taken in\n", " real time (True) or as fast as possible (False).\n", " The default is True.\n", " stable_step : bool, optional\n", " Boolean flag that indicates the type of real time stepping that\n", " should be used. The default is False.\n", "\n", " When real_time is False, this flag is ignored.\n", "\n", " When real_time is True and stable_step is False, the time of the\n", " first step() call since instantiation or reset is noted. Then, at\n", " every subsequent step() call, the function will sleep until it has\n", " been exactly dt*(n-1) seconds since the noted epoch. dt is the\n", " simulator time step and n is the number of times step() has been\n", " called since instantiation or reset. This guarantees a more\n", " accurate total run time, but less stable frame rates, especially\n", " if, at any point while running a simulation, there is a long pause\n", " between step() calls.\n", "\n", " When real_time and stable_step are True, the function will sleep\n", " until the duration since the last time step() was called is equal\n", " to the time step of the simulation. This guarantees a more stable\n", " frame rate, but less accurate total run time.\n", "\n", " Returns\n", " -------\n", " ret_code : int\n", " 0 if successful, -1 if something went wrong." ] }, { "cell_type": "code", "execution_count": 9, "id": "abb5f9bd-4031-42bd-a6b4-89b5e2b13e6b", "metadata": {}, "outputs": [], "source": [ "while proj.simtime <= 2.:\n", " proj.step(real_time=True, # Run the simulation in real time\n", " stable_step=False # Dynamically adjust the refresh rate for best total run time\n", " )" ] }, { "cell_type": "markdown", "id": "71b0554f-1573-4cae-9b6c-5c60e147ada5", "metadata": {}, "source": [ "When we are done using a `Project` object, it is imperative we call the `condynsate.Project.terminate` function. Doing so will save all recordings, if any were captured, and ensure all children threads exit gracefully.\n", "\n", " -----------------------------------------------------------------------------\n", " | condynsate.Project.terminate |\n", " -----------------------------------------------------------------------------\n", " Gracefully terminates the project and all children threads.\n", "\n", " Returns\n", " -------\n", " ret_code : int\n", " 0 if successful, -1 if something went wrong." ] }, { "cell_type": "code", "execution_count": 10, "id": "17c9a62e-9fa7-4248-9379-4f60d51cde9e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "proj.terminate() # Returns 0 on success" ] }, { "cell_type": "markdown", "id": "36d1e2de-e3f4-4dfb-a530-766a4420f876", "metadata": {}, "source": [ "## Challenge" ] }, { "cell_type": "markdown", "id": "5ff6e017-b96d-4668-879d-5ebaf083ae88", "metadata": {}, "source": [ "This tutorial is now complete. For an added challenge, think of how you would modify this project so that two cubes, one above the other, are loaded but only the top one has 6 degrees of freedom." ] } ], "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.14.0" } }, "nbformat": 4, "nbformat_minor": 5 }