Shiny for Python
Overview
Teaching: 60 min
Exercises: 0 minQuestions
Create highly interactive visualizations, realtime dashboards, data explorers, model demos, all in pure Python
Objectives
First learning objective. (FIXME)
Introduction
Shiny is a software package for building interactive web applications. Originally created for the R language, it has been now expanded to support Python Language. We will focus here on Shiny for Python. Most of the internals of Shiny are shared between the two implementations.
We start our exploration of shiny creating a virtual desktop on Thorny Flat using Open On-demand. Goto to Open On-Demand portal for Thorny Flat. Enter your credentials. From the dashboard select “Interactive Apps” and “Thorny Desktop”
Select minimal requests such as “standby” for partition, 4 hours walltime, 4 CPU cores. A new job will be created
Click on “Launch Thorny Desktop” and open 2 windows, a terminal, and a web browser.
Load the module for Python on the terminal
$> module load lang/python/cpython_3.11.3_gcc122
Now we are ready to create our first shiny app.
Create a folder for our shiny apps. Lets call it SHINY and inside create a folder app1
$> mkdir -p SHINY/app1
$> cd SHINY/app1
Now we can create a simple example app with the command::
$> shiny create .
Run the first app with:
$> shiny run --reload
On the web browser go to https://localhost:8000 to visualize the app
When we execute shiny create .
a python file called app.py
is created.
This file is as follows:
from shiny import App, render, ui
app_ui = ui.page_fluid(
ui.h2("Hello Shiny!"),
ui.input_slider("n", "N", 0, 100, 20),
ui.output_text_verbatim("txt"),
)
def server(input, output, session):
@output
@render.text
def txt():
return f"n*2 is {input.n() * 2}"
app = App(app_ui, server)
This file contains the main ingredients of a shiny app. Shiny applications consist of two parts: the user interface (or UI), and the server function. These are combined using a shiny.App object.
Adding UI inputs and outputs
Note the two UI pieces added:
input_slider()
creates a slider.output_text_verbatim()
creates a field to display dynamically generated text.
These elements have been added to the app_ui()
We can now copy this app and start adding modifications to it to construct our second app.
Cancel the execution of the first app executing ^C
, move one folder up, and copy the folder under a new name.
$> cd ..
$> cp -r app1 app2
$> cd app2
Change the file app.py
inside app2
as follows:
from shiny import App, render, ui
app_ui = ui.page_fluid(
ui.h2("Sum Shiny!"),
ui.input_slider("a", "A", 0, 100, 20),
ui.input_slider("b", "B", 0, 100, 20),
ui.output_text_verbatim("txt"),
)
def server(input, output, session):
@output
@render.text
def txt():
return f"a+b is {input.a() + input.b()}"
app = App(app_ui, server)
Now we have two sliders and one verbatim text. These elements are created as part of the app_ui()
function.
The UI part defines what visitors will see on their web page. The dynamic parts of our app happen inside the server function.
Inside the server function, we define an output function named txt. This output function provides the content for the output_text_verbatim(“txt”) in the UI.
Note that inside the server function, we do the following:
- define a function named
txt
, whose output shows up in the UI’s output_text_verbatim(“txt”). - decorate it with
@render.text
, to say the result is text (and not, e.g., an image). - decorate it with
@output
, to say the result should be displayed on the web page.
Finally, notice our txt() function takes the value of our sliders a and b, and returns its sum computed inside the txt
function. To access the value of the sliders, we use input.a()
and input.b()
. Notice that these are callable objects (like a function) that must be invoked to get the value.
Reactivity
This reactive flow of data from UI inputs to server code, and back out to UI outputs is central to how Shiny works.
When you moved the sliders in the app, a series of actions were kicked off, resulting in the output on the screen changes. This is called reactivity.
Inputs, like our sliders a
and b
, are reactive values: when they change, they automatically cause any of the reactive functions that use them (like txt()) to recalculate.
Layouts
So far, our UI has consisted of two inputs and one output. Shiny also has layout components that can contain other components and visually arrange them. Examples of these are sidebar layouts, tab navigation, and cards.
For our next example, we’ll use a common layout strategy for simple Shiny apps. We will have input controls in a narrow sidebar on the left and a plot on the right.
In the following code, we use the function ui.layout_sidebar() to separate the page into two panels. This function takes two arguments: a ui.panel_sidebar and a ui.panel_main, which each can contain whatever components you want to display on the left and right, respectively.
import matplotlib.pyplot as plt
import numpy as np
from shiny import ui, render, App
# Create some random data
t = np.linspace(0, 2 * np.pi, 1024)
data2d = np.sin(t)[:, np.newaxis] * np.cos(t)[np.newaxis, :]
app_ui = ui.page_fixed(
ui.h2("Playing with colormaps"),
ui.markdown("""
This app is based on a [Matplotlib example][0] that displays 2D data
with a user-adjustable colormap. We use a range slider to set the data
range that is covered by the colormap.
[0]: https://matplotlib.org/3.5.3/gallery/userdemo/colormap_interactive_adjustment.html
"""),
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_radio_buttons("cmap", "Colormap type",
dict(viridis="Perceptual", gist_heat="Sequential", RdYlBu="Diverging")
),
ui.input_slider("range", "Color range", -1, 1, value=(-1, 1), step=0.05),
),
ui.panel_main(
ui.output_plot("plot")
)
)
)
def server(input, output, session):
@output
@render.plot
def plot():
fig, ax = plt.subplots()
im = ax.imshow(data2d, cmap=input.cmap(), vmin=input.range()[0], vmax=input.range()[1])
fig.colorbar(im, ax=ax)
return fig
app = App(app_ui, server)
Notice how Shiny uses nested function calls to indicate parent/child relationships. In this example, ui.input_radio_buttons()
is inside of ui.panel_sidebar()
, and ui.panel_sidebar()
is in ui.layout_sidebar()
, and so on.
app_ui = ui.page_fixed(
ui.h2(...),
ui.markdown(...),
ui.layout_sidebar(
ui.panel_sidebar(
ui.input_radio_buttons(...),
ui.input_slider(...),
),
ui.panel_main(
ui.output_plot(...)
)
)
)
This example also includes a title and some explanatory text written in a Markdown string literal, and uses the ui.markdown()
function to render it to HTML.
UI elements for inputs
Now we will explore in more detail the creation of UI elements separated from the server logic.
Each input control on a page is created by calling a Python function. All such functions take the same first two string arguments:
id
: an identifier used to refer to input’s value in the server code. For example, id=”x1” corresponds with input.x1() in the server function. id values must be unique across all input and output objects on a page, and should follow Python variable/function naming rules (lowercase with underscores, alphanumeric characters allowed, cannot start with a number).label
: a description for the input that will appear next to it. Can usually be None if no label is desired.
Note that many inputs take additional arguments. Lets see some of the UI elements from our previous apps:
ui.input_slider("a", "A", 0, 100, 20),
ui.input_slider("b", "B", 0, 100, 20),
ui.input_radio_buttons("cmap", "Colormap type",
dict(viridis="Perceptual", gist_heat="Sequential", RdYlBu="Diverging"))
ui.input_slider("range", "Color range",
-1, 1, value=(-1, 1), step=0.05),
So far we have only use input_slider
and , input_radio_buttons
, but there are many other UI elements. We’ll show some common input objects.
Number inputs
ui.input_numeric
creates a text box where a number (integer or real) can be entered, plus up/down buttons. This is most useful when you want the user to be able to enter an exact value.
ui.input_slider
creates a slider. Compared to a numeric input, a slider makes it easier to scrub back and forth between values, so it may be more appropriate when the user does not have an exact value in mind to start with. You can also provide more restrictions on the possible values, as the min, max, and step size are all strictly enforced.
ui.input_slider
can also be used to select a range of values. To do so, pass a tuple of two numbers as the value argument instead of a single number.
Example (4.Number_Inputs/app.py
):
from shiny import ui, App
app_ui = ui.page_fluid(
ui.input_numeric("x1", "Number", value=10),
ui.input_slider("x2", "Slider", value=10, min=0, max=20),
ui.input_slider("x3", "Range slider", value=(6, 14), min=0, max=20)
)
app = App(app_ui, None)
Text inputs
Shiny includes three inputs for inputting string values.
ui.input_text
This is a simple textfield. Use it for shorter, single-line values.
ui.input_text_area
displays multiple lines, soft-wraps the text, and lets the user include line breaks, so is more appropriate for longer runs of text or multiple paragraphs.
ui.input_password
is for passwords and other values that should not be displayed in the clear. (Note that Shiny does not apply any encryption to the password, so if your app involves passing sensitive information, make sure your deployed app uses https, not http, connections.)
Example (5.Text_Inputs/app.py
):
from shiny import ui, App
app_ui = ui.page_fluid(
ui.input_text("x1", "Text", placeholder="Enter text"),
ui.input_text_area("x2", "Text area", placeholder="Enter text"),
ui.input_password ("x3", "Password", placeholder="Enter password"),
)
app = App(app_ui, None)
Selection inputs
There are two options for single and multiple selection from a list of values
ui.input_selectize
and ui.input_select
You can choose whether the user can select multiple values or not, using the multiple argument.
ui.input_selectize
uses the Selectize JavaScript library.
ui.input_select
inserts a standard HTML <select>
tag.
ui.input_radio_buttons
and ui.input_checkbox_group
are useful for cases where you want the choices to always be displayed. Radio buttons force the user to choose one and only one option, while checkbox groups allow zero, one, or multiple choices to be selected.
Example (6.Selection_inputs/app.py
):
from shiny import ui, App
choices = ["Tetrahedron", "Cube", "Octahedron", "Dodecahedron", "Icosahedron"]
app_ui = ui.page_fluid(
ui.h2("Platonic Solids"),
ui.input_selectize("x1", "Selectize (single)", choices),
ui.input_selectize("x2", "Selectize (multiple)", choices, multiple = True),
ui.input_select("x3", "Select (single)", choices),
ui.input_select("x4", "Select (multiple)", choices, multiple = True),
ui.input_radio_buttons("x5", "Radio buttons", choices),
ui.input_checkbox_group("x6", "Checkbox group", choices),
)
app = App(app_ui, None)
Toggle inputs
Toggles allow the user to specify whether something is true/false (or on/off, enabled/disabled, included/excluded, etc.).
ui.input_checkbox
shows a simple checkbox, while ui.input_switch
shows a toggle switch. These are functionally identical, but by convention, checkboxes should be used when part of a form that has a Submit or Continue button, while toggle switches should be used when they take immediate effect.
Example (7.Toggle_inputs/app.py
)
from shiny import ui, App
app_ui = ui.page_fluid(
ui.input_checkbox("x1", "Checkbox"),
ui.input_switch("x2", "Switch")
)
app = App(app_ui, None)
Date inputs
There are two inputs for dates:
ui.input_date
lets the user specify a date, either interactively or by typing it in.ui.input_date_range
is similar, but for cases where a start and end date are needed.
Example (8.Date_inputs/app.py
):
from shiny import ui, App
app_ui = ui.page_fluid(
ui.input_date("x1", "Date input"),
ui.input_date_range("x2", "Date range input"),
)
app = App(app_ui, None)
Action inputs
ui.input_action_button
and ui.input_action_link
let the user invoke specific actions on the server side.
Use ui.input_action_button
for actions that feels effectual: recalculating, fetching new data, applying settings, etc. Add class_=”btn-primary” to highlight important actions (like Submit or Continue), and class_=”btn-danger” to highlight dangerous actions (like Delete).
Use ui.input_action_link
for actions that feel more like navigation, like exposing a new UI panel, navigating through paginated results, or bringing up a modal dialog.
Example (9.Action_inputs/app.py
):
from shiny import ui, App
app_ui = ui.page_fluid(
ui.p(ui.input_action_button("x1", "Action button")),
ui.p(ui.input_action_button("x2", "Action button (Primary)", class_="btn-primary")),
ui.p(ui.input_action_button("x3", "Action button (Danger)", class_="btn-danger")),
ui.p(ui.input_action_link("x4", "Action link")),
)
app = App(app_ui, None)
UI elements for outputs
Outputs create a spot on the webpage to put results from the server.
At a minimum, all UI outputs require an id argument, which corresponds to the server’s output ID.
For example if you create this UI output:
ui.output_text("my_func")
You need a decorated function my_func
on the server side. Something like:
def server(input, output, session):
@output
@render.text
def my_func():
return "The result of simulation is ..."
Notice that the name of the function my_func()
matches the output ID; this is how Shiny knows how to connect each of the UI’s outputs with its corresponding logic in the server.
Notice also the relationship between the names ui.output_text()
and the decorator @render.text
. Shiny outputs generally follow this pattern of ui.output_XXX()
for the UI and @render.XXX
to decorate the output logic.
There are two kinds of renderings. Static and Interactive. With Static outputs you will not react to user interaction while for interactive outputs, the user can trigger reactions on the element.
Static plot output
Render static plots based on Matplotlib with ui.output_plot()
and @render.plot
. Plotting libraries built on Matplotlib, like seaborn and plotnine are also supported.
The function that @render.plot
decorates typically returns a Matplotlib Figure or plotnine ggplot
object, but @render.plot
does support other less common return types, and also supports plots generated through side-effects.
Example (10.Static_plot/app.py
):
import numpy as np
from shiny import ui, render, App
from matplotlib import pyplot as plt
app_ui = ui.page_fluid(
ui.h2("Random points on scatter plot"),
ui.input_slider("n", "Number of Points", value=50, min=10, max=100, step=10),
ui.output_plot("a_scatter_plot"),
)
def server(input, output, session):
@output
@render.plot
def a_scatter_plot():
x = np.random.rand(input.n())
y = np.random.rand(input.n())
return plt.scatter(x,y)
app = App(app_ui, server)
Simple table output
Render various kinds of data frames into an HTML table with ui.output_table()
and @render.table
.
The function that @render.table
decorates typically returns a pandas.DataFrame
, but object(s) that can be coerced to a pandas.DataFrame
via an obj.to_pandas()
method are also supported (this includes Polars data frames and Arrow tables).
Example (11.Table_outpu/app.py
):
from pathlib import Path
import pandas as pd
from shiny import ui, render, App
df = pd.read_csv(Path(__file__).parent / "iris.csv")
app_ui = ui.page_fluid(
ui.h2("Iris Dataset"),
ui.output_table("iris_dataset"),
)
def server(input, output, session):
@output
@render.table
def iris_dataset():
return df
app = App(app_ui, server)
This example requires the packages pandas
for converting the CSV file into a pandas dataframe and jinja2
displaying the table.
Interactive plots
Shiny supports interactive plotting libraries such as plotly. Here’s a basic example using plotly:
from shiny import ui, App
from shinywidgets import output_widget, render_widget
import plotly.express as px
import plotly.graph_objs as go
df = px.data.iris()
app_ui = ui.page_fluid(
ui.h2("Iris Dataset"),
ui.div(
ui.input_select(
"x", label="Variable",
choices=['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
),
ui.input_select(
"color", label="Color",
choices=["species"]
),
class_="d-flex gap-3"
),
output_widget("my_widget")
)
def server(input, output, session):
@output
@render_widget
def my_widget():
fig = px.histogram(
df, x=input.x(), color=input.color(),
marginal="rug"
)
fig.layout.height = 275
return fig
app = App(app_ui, server)
For running this example we need the packages plotly
and shinywidgets
.
Interactive maps
Shiny supports interactive mapping libraries such as ipyleaflet, pydeck, and more. Here’s a basic example using ipyleaflet with a few basemaps:
Example (13.Interactive_maps/app.py
):
from shiny import *
from shinywidgets import output_widget, render_widget
import ipyleaflet as L
basemaps = {
"OpenStreetMap": L.basemaps.OpenStreetMap.Mapnik,
"Stamen.Toner": L.basemaps.Stamen.Toner,
"Stamen.Terrain": L.basemaps.Stamen.Terrain,
"Stamen.Watercolor": L.basemaps.Stamen.Watercolor,
"Esri.WorldImagery": L.basemaps.Esri.WorldImagery,
"Esri.NatGeoWorldMap": L.basemaps.Esri.NatGeoWorldMap,
}
app_ui = ui.page_fluid(
ui.h2("West Virginia - Mountaineer Country"),
ui.input_select(
"basemap", "Choose a basemap",
choices=list(basemaps.keys())
),
output_widget("map")
)
def server(input, output, session):
@output
@render_widget
def map():
basemap = basemaps[input.basemap()]
return L.Map(basemap=basemap, center=[39.6, -79.9], zoom=10)
app = App(app_ui, server)
Text output
Use ui.output_text()
and the correspoding decorators @render.text
to render a block of text.
Your server logic should return a Python string. No HTML markup or Markdown can be used, the text will be displayed without processing.
Example (14.Text_output/app.py
`):
from shiny import ui, render, App
app_ui = ui.page_fluid(
ui.output_text("my_cool_text")
)
def server(input, output, session):
@output
@render.text
def my_cool_text():
return "hello, world!"
app = App(app_ui, server)
There is also the variant ui.output_text_verbatim
with the same decorator @render.text
.
This is similar to text output, but renders text in a monospace font, and respects newlines and multiple spaces (unlike ui.output_text()
, which collapses all whitespace into a single space, in accordance with HTML’s normal whitespace rule).
Example (15.Text_verbatim_output/app.py
):
from shiny import ui, render, App
import scipy.special
app_ui = ui.page_fluid(
ui.output_text_verbatim("a_code_block"),
# The p-3 CSS class is used to add padding on all sides of the page
class_="p-3"
)
def server(input, output, session):
@output
@render.text
def a_code_block():
# This function should return a string
return scipy.special.__doc__
app = App(app_ui, server)
HTML and UI output
ui.output_ui()
and decorator @render.ui
are used to render HTML and UI from the server.
You’ll need to use output_ui
if you want to render HTML/UI dynamically–that is, if you want the HTML to change as inputs and other reactives change.
Example (16.HTML_UI_output/app.py
):
from shiny import ui, render, App
app_ui = ui.page_fluid(
ui.input_text("message", "Message", value="Hello, world!"),
ui.input_checkbox_group("styles", "Styles", choices=["Bold", "Italic"]),
ui.input_selectize("size", "HTML font size", choices=["H1", "H2", "H3", "H4"]),
# The class_ argument is used to enlarge and center the text
ui.output_ui("some_html", class_="display-3 text-center")
)
def server(input, output, session):
@output
@render.ui
def some_html():
x = input.message()
if "Bold" in input.styles():
x = ui.strong(x)
if "Italic" in input.styles():
x = ui.em(x)
if "H1" in input.size():
x = ui.h1(x)
if "H2" in input.size():
x = ui.h2(x)
if "H3" in input.size():
x = ui.h3(x)
if "H4" in input.size():
x = ui.h4(x)
return x
app = App(app_ui, server)
The function under the decorator @render.ui
can return any of the following:
- A plain string (which will be rendered as plain text, even if it contains HTML markup)
- Any HTML tag object (like
ui.tags.div()
) - Any Shiny UI object, including layouts, inputs, and outputs
Server logic
In Shiny, the server logic is defined within a function which takes three arguments: input, output, and session. It looks something like this:
def server(input, output, session):
# Server code goes here
...
All of the server logic we’ll discuss, such as using inputs and define outputs, happens within the server function.
Each time a user connects to the app, the server function executes once — it does not re-execute each time someone moves a slider or clicks on a checkbox. So how do updates happen? When the server function executes, it creates a number of reactive objects that persist as long as the user stays connected — in other words, as long is their session is active. These reactive objects are containers for functions that automatically re-execute in response to changes in inputs.
Defining outputs
To define the logic for an output, create a function with no parameters whose name matches a corresponding output ID in the UI. Then apply a render decorator and the @output decorator.
As the function is inside the server
and the server has input and output objects, these objects are visible inside the server.
However, input values can not be read at the top level of the server function. If you try to do that, you’ll get an error that says RuntimeError: No current reactive context. The input values are reactive and, as the error suggests, are only accessible within reactive code.
Consider this example (``17.server_logic/app.py`):
from shiny import App, render, ui
app_ui = ui.page_fluid(
ui.input_checkbox("enable", "Enable?"),
ui.h3("Is it enabled?"),
ui.output_text_verbatim("txt"),
)
def server(input, output, session):
@output
@render.text
def txt():
if input.enable():
return "Yes!"
else:
return "No!"
app = App(app_ui, server)
When you define an output function, Shiny makes it reactive, and so it can be used to access input values.
Key Points
First key point. Brief Answer to questions. (FIXME)
PyVista
Overview
Teaching: 60 min
Exercises: 0 minQuestions
3D plotting and mesh analysis with Python
Objectives
First learning objective. (FIXME)
FIXME
Key Points
First key point. Brief Answer to questions. (FIXME)
PyMunk
Overview
Teaching: 60 min
Exercises: 0 minQuestions
Pythonic 2d physics library for physics demos
Objectives
Learn the foundations of pymunk for creating 2D physical simulations of interacting bodies
Introduction to Pymunk
This lesson is intended to provide educators an introduction to Pymunk, a Python package that can create interactive kinematics simulations in two dimensions. Using this package, an educator can create simulations that demonstrate basics physics concepts like energy and momentum; these simulations can be modified by students in order to gain a deeper understanding of the underlying concepts. By using computer simulations, it is possible to let students explore more intricate systems than they might otherwise due to factors such as a limited number of physical setups. It can be especially helpful simulating systems that tend to produce incredibly noisy data when done with physical equipment.
Pymunk
Pymunk is a software package that provides accessible tools to perform simulations of classical physical systems in two dimension. It is object-oriented; that is, it has classes like Space
, Body
, Shape
, and Constraint
. We will be thus be constructing objects that belong to these classes. These objects then have properties (e.g. Body
has properties like mass and moment of intertia) as well as methods that will allow us to access and change those properties.
A note on units
The units in Pymunk do not represent anything physical but are merely arbitrary units. As such, it is not critical to use familiar values from real-world physics. It is often much more useful to use arbitrary values, adjusting them as we test our code to ensure that a viewer can readily understand the visualized output of the simulation.
The basics
Space
One of the most important concepts in Pymunk is the Space object. This is the two-dimensional region in which our simulation will take place, and almost every simulation will start by constructing this object:
space = pymunk.Space() # This does not usually take in arguments
Next it is important to set the gravity in the space:
space.gravity = (x,y)
Note that this function takes in two arguments, representing the strength of a uniform gravitational field in the $x$ and $y$ directions, respectively. Typically, if we are representing a top-down view of a system, both of these values will be zero; if we are representing a side-on view of a system, $x$ will be 0 and $y$ will be negative, representing a downward pull.
Bodies
Once we have a Space, we will populate it with bodies. When we construct these, we can provide their mass, moment of inertia. By default, these bodies are DYNAMIC
, moving around and reacting to forces. However, we can also make STATIC
bodies that cannot move.
Shapes
Unless a body is a point mass, we must construct a shape
associated with it. This shape defines the physical extent of the body, which is important for collisions. This object also has the friction
property, allowing control over the coefficient of kinetic friction. Pymunk uses the Coulomb friction model that is commonly taught in introductory physics courses, where the force due to friction is $F_f = \mu_k F_N$, where $\mu_k$ is the coefficient of kinetic friction and $F_N$ is the normal force.
Installing Pymunk
This notebook assumes that the user has already installed Python as well as Pymunk and its requirements. If this is being used on WVU’s Thorny Flat cluster using OnDemand with the most recent version of Python, this will already be the case. Otherwise, you must install it yourself.
Once python is installed, Pymunk can be installed with pip install pymunk
If you want to use Pymunk with Conda, use the command conda install -c conda-forge pymunk
Getting Started
We will start with a basic simulation of two balls colliding to introduce the essential functions in Pymunk. By doing this experiment computationally, we are able to eliminate several common sources of error that we would encounter in the real world, such as friction or performing the experiment on an uneven surface.
Here, we will establish the basic approach that we will follow throughout the other examples.
- Establish the space
- Define bodies, give them shapes, and then add them to the space
- Step the simulation forward in time, animating it
%reset -f
%matplotlib notebook
import pymunk as pm
from pymunk.vec2d import Vec2d
import matplotlib.pyplot as plt
import pymunk.matplotlib_util as pmplt
from matplotlib import animation as anm
space = pm.Space()
# space.gravity = (-500,0)
collision_elasticity = 1.0 # ranges from 0 (perfectly inelastic) to 1 (perfectly elastic)
mass1 = 10
radius1 = 10
ball1_body = pm.Body(mass=mass1,moment=pm.moment_for_circle(mass1, 0, radius1, (0,0)))
ball1_body.position = (100,100)
ball1_body.start_position = Vec2d(*ball1_body.position)
# ball1_body.apply_impulse_at_local_point(Vec2d(+12000, 0))
ball1_shape = pm.Circle(ball1_body,radius1)
ball1_shape.elasticity = collision_elasticity
space.add(ball1_body,ball1_shape)
mass2 = 10
radius2 = 10
ball2_body = pm.Body(mass=mass2,moment=pm.moment_for_circle(mass2, 0, radius2, (0,0)))
ball2_body.position = (250,100)
ball2_body.start_position = Vec2d(*ball2_body.position)
ball2_shape = pm.Circle(ball2_body,radius2)
ball2_shape.elasticity = collision_elasticity
space.add(ball2_body,ball2_shape)
fig = plt.figure()
ax = plt.axes(xlim=(0, 500), ylim=(0, 200))
ax.set_aspect("equal")
o = pmplt.DrawOptions(ax)
# space.debug_draw(o)
space.shapes[0].body.apply_impulse_at_local_point((5000,0))
def init():
space.debug_draw(o)
return []
steps_per_second = 200
substeps = 10
def animate(dt):
for x in range(substeps):
space.step(1/steps_per_second/substeps)
ax.clear()
ax.set_xlim(0,500)
ax.set_ylim(0,200)
space.debug_draw(o)
return []
frames = 150
anim = anm.FuncAnimation(fig, animate, init_func=init,
frames=frames, interval=20, blit=False, repeat=False)
<IPython.core.display.Javascript object>
While we can ignore factors like friction and sloped surfaces if we want, we also have the power the change the simulation an environment to mimic these if we so choose.
For example, suppose we wanted to examine how the collision changes if our “table” were sloped to the left. We can simulate this by applying gravity in the $-x$ direction.
We can also use the elasticity parameter of the bodies to explore inelastic, perfectly elastic, and super elastic collisions.
Simple pendulum
Next, we’ll introduce the idea of connecting objects together by making a simple pendulum. We accomplish this using a constraint
object, specifically the PinJoint
. This allows us to connect two bodies together with a perfectly rigid, massless rod. For this example, we will connect our circle to a static point in space. It is important to note that we must supply coordinates for where on each body we attach the PinJoint; these coordinates are in each bodies own coordinate system. That is, the coordinates for the the connection to space.static_body
are the same as for the space as a whole, but the coordinates for the connection to the circle are measured from the center of that circle.
We can also easily explore the damped oscillator by applying damping to the whole space. When we change the damping
property of Space
, we are periodically reducing the velocity of every body in the space.
%reset -f
%matplotlib notebook
import matplotlib.pyplot as plt
from matplotlib import animation as anm
import pymunk as pm
from pymunk.vec2d import Vec2d
import pymunk.matplotlib_util as pmplt
space = pm.Space()
space.gravity = 0,-9820
# space.damping = 0.2
mass = 10
radius = 25
body = pm.Body(mass,pm.moment_for_circle(mass, 0, radius, (0,0)))
body.position = (300, 175)
body.start_position = Vec2d(*body.position)
shape = pm.Circle(body, radius)
space.add(body, shape)
pj = pm.PinJoint(space.static_body, body, (300, 425), (0,0))
space.add(pj)
fig = plt.figure()
ax = plt.axes(xlim=(0, 600), ylim=(0, 600))
ax.set_aspect("equal")
o = pmplt.DrawOptions(ax)
space.shapes[0].body.apply_impulse_at_local_point((-12000,0))
def init():
space.debug_draw(o)
return []
steps_per_second = 200
substeps = 10
def animate(dt):
for x in range(substeps):
space.step(1/steps_per_second/substeps)
ax.clear()
ax.set_xlim(0,600)
ax.set_ylim(0,600)
space.debug_draw(o)
return []
frames = 500
anim = anm.FuncAnimation(fig, animate, init_func=init,
frames=frames, interval=20, blit=False, repeat=False)
<IPython.core.display.Javascript object>
Coupled Oscillators
One possible use of Pymunk is to simulate coupled oscillators like the double pendulum. While this system is straightforward to build physically, it can be tricky to reliably start the pendulum in a way that demonstrates the principles you’re covering. Students are frequently first taught the math behind the double pendulum in two dimension, and the focus is usually on the fundamental modes of the system. However, it can be difficult to reliably get the pendulum swinging in a way that demonstrates these principles. It’s very easy to accidentally to start the pendulum in a way that swings in three dimensions, and some of the fundamental modes can require precise angles.
Pymunk can help solve both of these problems. We can simulate a basic double pendulum by modifying our example from above, adding another circle below the first and placing a second PinJoint between them.
%reset -f
# %matplotlib notebook
import matplotlib.pyplot as plt
from matplotlib import animation as anm
import pymunk as pm
from pymunk.vec2d import Vec2d
import pymunk.matplotlib_util as pmplt
space = pm.Space()
space.gravity = (0,-9820)
mass = 10
radius = 25
body1 = pm.Body(mass,pm.moment_for_circle(mass, 0, radius, (0,0)))
body1.position = (300, 375)
# body1.position = (275, 350)
body1.start_position = Vec2d(*body1.position)
shape1 = pm.Circle(body1, radius)
space.add(body1, shape1)
pj1 = pm.PinJoint(space.static_body, body1, (300, 550), (0,0))
space.add(pj1)
mass = 10
radius = 25
body2 = pm.Body(mass,pm.moment_for_circle(mass, 0, radius, (0,0)))
body2.position = (300, 175)
# body2.position = (250, 150)
body2.start_position = Vec2d(*body2.position)
shape2 = pm.Circle(body2, radius)
space.add(body2, shape2)
pj2 = pm.PinJoint(body1, body2, (0,0), (0,0))
space.add(pj2)
fig = plt.figure()
ax = plt.axes(xlim=(0, 600), ylim=(0, 600))
ax.set_aspect("equal")
o = pmplt.DrawOptions(ax)
space.shapes[0].body.apply_impulse_at_local_point((-12000,0))
space.shapes[1].body.apply_impulse_at_local_point((20000,0))
def init():
space.debug_draw(o)
return []
steps_per_second = 100
substeps = 10
def animate(dt):
for x in range(substeps):
space.step(1/steps_per_second/substeps)
ax.clear()
ax.set_xlim(0,600)
ax.set_ylim(0,600)
space.debug_draw(o)
return []
frames = 1000
anim = anm.FuncAnimation(fig, animate, init_func=init,
frames=frames, interval=20, blit=False, repeat=False)
<IPython.core.display.Javascript object>
This is an example of a chaotic system; that is, the exact path that the masses will change dramatically with only a small change to our initial conditions. There are also fundamental modes to this system, in which the pendulum will oscillate in a very predictable manner.
##
Projectile Motion
Next, we’ll simulate a common projectile motion demo that demonstrates the equal pull of gravity on all objects, regardless of their mass. This is sometimes called “Monkey and Hunter”; the premise is that some projectile is being aimed at a target, but the target is released from rest at the same moment that the projectile is being released. However, the person aiming the projectile is aware of this, and wants to know where they should aim to still hit the target. When analyzing this scenario, we find that if the projectile is aimed at the starting location of the target, then the projectile will hit the target.
%reset -f
%matplotlib notebook
import pymunk as pm
from pymunk.vec2d import Vec2d
import matplotlib.pyplot as plt
import pymunk.matplotlib_util as pmplt
from matplotlib import animation as anm
space = pm.Space()
space.gravity = (0,-9800)
mass1 = 10
radius1 = 10
ball1_body = pm.Body(mass=mass1,moment=pm.moment_for_circle(mass1, 0, radius1, (0,0)))
ball1_body.position = (100,100)
ball1_body.start_position = Vec2d(*ball1_body.position)
ball1_body.apply_impulse_at_local_point(Vec2d(17500, 17500))
ball1_shape = pm.Circle(ball1_body,radius1)
space.add(ball1_body,ball1_shape)
mass2 = 50
radius2 = 20
ball2_body = pm.Body(mass=mass2,moment=pm.moment_for_circle(mass2, 0, radius2, (0,0)))
ball2_body.position = (500,500)
ball2_body.start_position = Vec2d(*ball2_body.position)
ball2_shape = pm.Circle(ball2_body,radius2)
space.add(ball2_body,ball2_shape)
fig = plt.figure()
ax = plt.axes(xlim=(0, 600), ylim=(0, 600))
ax.set_aspect("equal")
o = pmplt.DrawOptions(ax)
def init():
space.debug_draw(o)
return []
steps_per_second = 200
substeps = 10
def animate(dt):
for x in range(substeps):
space.step(1/steps_per_second/substeps)
ax.clear()
ax.set_xlim(0,600)
ax.set_ylim(0,600)
space.debug_draw(o)
return []
frames = 50
anim = anm.FuncAnimation(fig, animate, init_func=init,
frames=frames, interval=20, blit=False, repeat=False)
<IPython.core.display.Javascript object>
Mass on a Spring
Here, we will demonstrate another constraint object, the damped spring. This is another example of an oscillator, although there are many other demos that can be done using springs. This constraint object works similarly to the previous PinJoint; it connects two objects from user-defined points on those objects and limits how the distance between them can change. However, because springs provide a restoring force instead of simply forcing the objects to stay a fixed difference apart, we must provide some extra parameters to this object. We must define the rest_length
(the length of the spring when no force is applied); the stiffness
(how much the spring stretches or compresses when a force is applied); and the damping
, which will gradually slow our oscillating mass down.
%reset -f
%matplotlib notebook
import pymunk as pm
from pymunk.vec2d import Vec2d
import matplotlib.pyplot as plt
import pymunk.matplotlib_util as pmplt
from matplotlib import animation as anm
space = pm.Space()
# space.gravity = (0,-9000)
mass1 = 10
radius1 = 10
body = pm.Body(mass=mass1,moment=pm.moment_for_circle(mass1, 0, radius1, (0,0)))
body.position = (250,100)
body.start_position = Vec2d(*body.position)
body.apply_impulse_at_local_point(Vec2d(12000, 0))
shape = pm.Circle(body,radius1)
space.add(body,shape)
spring = pm.DampedSpring(space.static_body, body, (50, 100), (0,0), rest_length=200,stiffness=1500,damping=10)
# spring = pm.DampedSpring(space.static_body, body, (50, 100), (0,0), rest_length=200,stiffness=1500,damping=500) # overdamped
space.add(spring)
fig = plt.figure()
ax = plt.axes(xlim=(0, 500), ylim=(0, 200))
ax.set_aspect("equal")
o = pmplt.DrawOptions(ax)
# space.debug_draw(o)
space.shapes[0].body.apply_impulse_at_local_point((5000,0))
def init():
space.debug_draw(o)
return []
steps_per_second = 200
substeps = 10
def animate(dt):
for x in range(substeps):
space.step(1/steps_per_second/substeps)
ax.clear()
ax.set_xlim(0,500)
ax.set_ylim(0,200)
space.debug_draw(o)
return []
frames = 1000
anim = anm.FuncAnimation(fig, animate, init_func=init,
frames=frames, interval=20, blit=False, repeat=False)
<IPython.core.display.Javascript object>
By changing the damping value, we can explore undamped systems, as well as underdamping, critical damping, and overdamping.
Key Points
The main concepts in pymunk are the space, body, shape and joint
Pymunk take charge of the physical simulations leaving the representation to other libraries