This repo serves a Plotly Dash application built on Flask and deployed on Heroku from GitHub. Active development is modernizing the app while preserving the current public deployment path.
It is available at: www.double-pendulum.net
The application is an extension of Double_Pendulum, which derived the symbolic equations of motion.
For the current modernization direction, see ROADMAP.md. For
durable production architecture and workflow notes, see
documentation/.
Both models move in the
Simple Model:
- The rods
$OP_1$ and$P_1P_2$ are rigid, massless and inextensible. - Masses at
$P_1$ and$P_2$ are subject to a uniform acceleration due to gravity.
Compound Model:
- Masses
$M_1$ and$M_2$ are uniformly distributed along rod lengths$l_1$ and$l_2$ . - Each rod is subject to a rotational kinetic energy and moment of inertia about its centre of mass.
- Applying the parallel axis theorem, we account for the rotational dynamics about the pendulum ends.
The effects of any dissipative forces such as air resistance or friction are neglected therefore, the total mechanical energy is conserved.
For this conservative system, the equations of motion are derived from Lagrangian,
Where
Lagrangian System
We solve the Euler-Lagrange equations for
The result is a system of
Uncoupling the equations involves extensive algebra and can be found in this Python derivation
Hamiltonian System
The Hamiltonian
Here, generalised velocities are replaced with generalised momenta and we form the equations of motion,
Hamilton's equations are first-order and the Python derivation proved simpler than uncoupling the Euler-Lagrange equations.
In this instance, the Hamiltonian was the first integral of the Lagrangian, representing total energy of the system. For
Closed-form, analytical solutions of the double pendulum system are not known to exist. The system must be integrated numerically.
This application represents a hybrid of web-development and dashboard engineering. The active architecture is a Dash application shell with callback-owned interaction state, Python backend simulation/model code, and a Canvas-based Simulation renderer fed by Python-built payloads.
- Deriving the equations:
- The equations of motion are derived symbolically with
SymPyand abstracted as reusable helpers undersrc/double_pendulum/math/. - A simple conditional logic structure controls which model is derived.
- The equations of motion are derived symbolically with
- DoublePendulum Class:
- Instantiating a Lagrangian or Hamiltonian double-pendulum model object; clicking the
Run Simulationbutton, derives the symbolic equations "on-the-fly". - The equations are cached to reduce runtime for further simulations of the same model.
- The equations are numerically integrated using
SciPy's solve_ivp function. Integrator arguments are available in the class structure but this functionality is yet to be added to the UI.
- Instantiating a Lagrangian or Hamiltonian double-pendulum model object; clicking the
- Visualisation:
- The production Simulation page now uses a Canvas renderer for motion, angular displacement, and angular state projection playback from Python-built payloads.
PlotlyandMatplotlibremain dependencies for retained plotting helpers and future richer analytical inspection, but legacy Plotly outputs are no longer generated in the normal Simulation run flow.MathJaxAPI is used for rendering latex expressions.
- Error Handling:
- Robust validation of user inputs, ensures computational load is never too high.
The Canvas integration does not imply that the numerical science is fully validated. Energy diagnostics, chaos diagnostics, tolerance sensitivity, solver-method equivalence, and long-duration scientific validity remain deferred modernization work.
Double_Pendulum_App/
├── assets/
│ ├── Images/
│ │ ├── github-mark.png
│ │ ├── Model_Compound_Transparent_NoText.png
│ │ ├── Model_Simple_Transparent_NoText.png
│ │ ├── Models_Joint_White.png
│ │ └── Screenshot.png
│ ├── MarkdownScripts/
│ │ ├── information.txt
│ │ ├── mathematics_hamiltonian.txt
│ │ └── mathematics_lagrangian.txt
│ ├── custom-header.html
│ ├── nav-bar.js
│ ├── scroll.js
│ ├── simulation-canvas-renderer.js
│ └── styles.css
├── app/
│ ├── callbacks/
│ ├── components/
│ ├── content/
│ ├── pages/
│ └── serialization/
├── documentation/
├── src/
│ └── double_pendulum/
│ ├── math/
│ ├── models/
│ ├── plotting/
│ └── validation/
├── tests/
├── LICENSE.md
├── ROADMAP.md
├── pendulum_app.py
├── Procfile
├── README.md
├── requirements.txt
└── .python-version
- Deployment/runtime files:
Procfilespecifies the command to run the app..python-versiondefines the Python runtime for Heroku deployment.requirements.txtlists necessary dependencies.runtime.txthas intentionally been removed and should not be reintroduced.
- Dash applications automatically read and serve files located in the root of the assets/ directory:
custom-header.html- Defines the page meta-data.nav-bar.js- Script to scroll to the top of the math pages.scroll.js- Script triggered by the "Run Simulation" button scrolls to the input/figures section of the page.simulation-canvas-renderer.js- Browser-side Canvas rendering, playback, and selected-frame inspection for Python-built simulation payloads.styles.css- Handles app styles such as fonts, colours, media queries, and layout structure.
- The
app/package contains Dash-facing application code:app/pages/owns route-level page layouts.app/components/owns reusable UI shell, controls, graph wrappers, references, and figure styling.app/content/owns user-facing copy, page metadata, markdown paths, and reference data.app/callbacks/owns routing and simulation callback registration.app/serialization/owns the Canvas payload API used by the Simulation page.
- The
src/double_pendulum/package is the home for reusable simulation, symbolic math, validation, and plotting/helper logic. - The
documentation/directory is the durable architecture/workflow reference for production-facing decisions, including the current Canvas Simulation architecture underdocumentation/simulation-canvas/.
The chaos/non-linear dynamics page is a work in progress.
The active product and architecture direction is tracked in ROADMAP.md. The local development/ directory may contain ignored exploratory/reference work, including Simulation Workbench, math-fidelity, and solver-contract evidence. Durable summaries live under documentation/, and accepted findings should be encoded in production code and tracked tests.
The current roadmap continues from the deployed Simulation baseline. Phase 9 established the production layout and deployment hooks; Phase 10 is focused on the chaos-analysis framework before any production Chaos page redesign.
Future chaos work aims to:
- Produce a semi-structured database of angles, velocities, positions, and momenta within specified bounds using the Hamiltonian derivation.
- Produce bifurcation diagrams, and Poincaré sections to qualitatively analyse periodicity.
- Analyse the truncation error of the numerical integration.
- Analyse orbits quantitatively using Lyapunov exponents.
- Fork the Double Pendulum App repository on GitHub.
- Clone your forked repository:
git clone <your-forked-repo-url>
cd Double_Pendulum_Apppython3.12 -m venv .venv
source .venv/bin/activate # On macOS/Linux
.\.venv\Scripts\activate # On Windowspip install -r requirements.txtLocal HTTP development does not redirect by default. Deployment-specific HTTPS
redirects are controlled by the FORCE_HTTPS environment flag in
app/config.py; set FORCE_HTTPS=true only when the server
should redirect plain HTTP requests to HTTPS. The direct local run debug flag is
controlled separately with DASH_DEBUG; leave it false for deployment-style
checks and set DASH_DEBUG=true only for local development.
For the existing Heroku deployment, keep using the tracked Procfile:
web: gunicorn pendulum_app:serverSet deployment config through Heroku config vars rather than editing
pendulum_app.py:
heroku config:set FORCE_HTTPS=true
heroku config:unset DASH_DEBUGpython pendulum_app.pyFor Dash debug mode during local development:
DASH_DEBUG=true python pendulum_app.py6. Access the app at http://127.0.0.1:8050/ (The development server)
Before starting the Dash app for a smoke check, confirm port 8050 is not
already occupied:
lsof -nP -iTCP:8050 -sTCP:LISTENIf port 8050 is already occupied, do not blindly kill the process. Identify
it if possible, or choose a clearly documented temporary port only when
appropriate.
If you start the Dash development server, capture the process ID, stop that exact process after testing, and verify you did not leave a Flask/Dash process running. This cleanup rule is more important than completing a browser smoke check.
Install the app and test dependencies in the Python 3.12 virtual environment:
pip install -r requirements.txt
pip install -r requirements-dev.txtRun the full test suite with:
python -m pytestFor Simulation UI work, a manual/browser smoke check should cover a simple
valid run, Canvas rendering, playback controls, stale state after input changes,
and invalid-input validation. See
documentation/development-workflow.md
for the full safe smoke-test procedure.
The current test layout is organized by purpose:
tests/unit/covers validation and lightweight symbolic fidelity checks.tests/integration/covers app import, route layout smoke behavior, and the Flaskserverobject used by Gunicorn.tests/numerical/covers basic Lagrangian and Hamiltonian simulation shape, finite values, and initial-condition consistency.
Validation scope note: these tests support deployment and modernization work, but they are not a complete numerical validation project. Full derivation audits, compound-equation symbolic checks, energy-conservation tolerances, trajectory regression fixtures, and a Hamiltonian state/input convention audit remain scientific-validation work rather than deployment blockers.
dashdash-bootstrap-components
Flaskgunicorn
numpyscipysympy
matplotlibplotly
The requirements.txt file intentionally lists only top-level application/runtime dependencies. The old fully frozen dependency list is preserved in requirements-old-freeze.txt for reference.
Development and test-only dependencies are listed separately in requirements-dev.txt.

