Repository: Matheart/manim-physics
Branch: main
Commit: 2876741c43e5
Files: 54
Total size: 76.0 KB
Directory structure:
gitextract_3nqz4oat/
├── .github/
│ ├── manimdependency.json
│ ├── scripts/
│ │ └── ci_build_cairo.py
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── README.md
├── docs/
│ ├── Makefile
│ ├── make.bat
│ ├── requirements.txt
│ ├── rtd-requirements.txt
│ └── source/
│ ├── _static/
│ │ ├── custom.css
│ │ └── responsiveSvg.js
│ ├── _templates/
│ │ └── autosummary/
│ │ ├── class.rst
│ │ └── module.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── contributing.rst
│ ├── index.rst
│ ├── reference.rst
│ └── reference_index/
│ ├── electromagnetism.rst
│ ├── optics.rst
│ ├── rigid_mechanics.rst
│ └── wave.rst
├── example.py
├── manim_physics/
│ ├── __init__.py
│ ├── electromagnetism/
│ │ ├── __init__.py
│ │ ├── electrostatics.py
│ │ └── magnetostatics.py
│ ├── optics/
│ │ ├── __init__.py
│ │ ├── lenses.py
│ │ └── rays.py
│ ├── rigid_mechanics/
│ │ ├── __init__.py
│ │ ├── pendulum.py
│ │ └── rigid_mechanics.py
│ └── wave.py
├── pyproject.toml
└── tests/
├── __init__.py
├── conftest.py
├── control_data/
│ ├── electromagnetism/
│ │ ├── electric_field.npz
│ │ ├── magnetic_field.npz
│ │ └── magnetic_field_multiple_wires.npz
│ ├── optics/
│ │ └── rays_lens.npz
│ ├── pendulum/
│ │ ├── multipendulum.npz
│ │ └── pendulum.npz
│ ├── rigid_mechanics/
│ │ └── rigid_mechanics.npz
│ └── waves/
│ ├── linearwave.npz
│ ├── radialwave.npz
│ └── standingwave.npz
├── test_electromagnetism.py
├── test_lensing.py
├── test_pendulum.py
├── test_rigid_mechanics.py
└── test_wave.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/manimdependency.json
================================================
{
"windows": {
"tinytex": [
"standalone",
"preview",
"doublestroke",
"ms",
"everysel",
"setspace",
"rsfs",
"relsize",
"ragged2e",
"fundus-calligra",
"microtype",
"wasysym",
"physics",
"dvisvgm",
"jknapltx",
"wasy",
"cm-super",
"babel-english",
"gnu-freefont",
"mathastext",
"cbfonts-fd"
]
},
"macos": {
"tinytex": [
"standalone",
"preview",
"doublestroke",
"ms",
"everysel",
"setspace",
"rsfs",
"relsize",
"ragged2e",
"fundus-calligra",
"microtype",
"wasysym",
"physics",
"dvisvgm",
"jknapltx",
"wasy",
"cm-super",
"babel-english",
"gnu-freefont",
"mathastext",
"cbfonts-fd"
]
}
}
================================================
FILE: .github/scripts/ci_build_cairo.py
================================================
# Logic is as follows:
# 1. Download cairo source code: https://cairographics.org/releases/cairo-<version>.tar.xz
# 2. Verify the downloaded file using the sha256sums file: https://cairographics.org/releases/cairo-<version>.tar.xz.sha256sum
# 3. Extract the downloaded file.
# 4. Create a virtual environment and install meson and ninja.
# 5. Run meson build in the extracted directory. Also, set required prefix.
# 6. Run meson compile -C build.
# 7. Run meson install -C build.
import hashlib
import logging
import os
import subprocess
import sys
import tarfile
import tempfile
import typing
import urllib.request
from contextlib import contextmanager
from pathlib import Path
from sys import stdout
CAIRO_VERSION = "1.18.0"
CAIRO_URL = f"https://cairographics.org/releases/cairo-{CAIRO_VERSION}.tar.xz"
CAIRO_SHA256_URL = f"{CAIRO_URL}.sha256sum"
VENV_NAME = "meson-venv"
BUILD_DIR = "build"
INSTALL_PREFIX = Path(__file__).parent.parent.parent / "third_party" / "cairo"
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
def is_ci():
return os.getenv("CI", None) is not None
def download_file(url, path):
logger.info(f"Downloading {url} to {path}")
block_size = 1024 * 1024
with urllib.request.urlopen(url) as response, open(path, "wb") as file:
while True:
data = response.read(block_size)
if not data:
break
file.write(data)
def verify_sha256sum(path, sha256sum):
with open(path, "rb") as file:
file_hash = hashlib.sha256(file.read()).hexdigest()
if file_hash != sha256sum:
raise Exception("SHA256SUM does not match")
def extract_tar_xz(path, directory):
with tarfile.open(path) as file:
file.extractall(directory)
def run_command(command, cwd=None, env=None):
process = subprocess.Popen(command, cwd=cwd, env=env)
process.communicate()
if process.returncode != 0:
raise Exception("Command failed")
@contextmanager
def gha_group(title: str) -> typing.Generator:
if not is_ci():
yield
return
print(f"\n::group::{title}")
stdout.flush()
try:
yield
finally:
print("::endgroup::")
stdout.flush()
def set_env_var_gha(name: str, value: str) -> None:
if not is_ci():
return
env_file = os.getenv("GITHUB_ENV", None)
if env_file is None:
return
with open(env_file, "a") as file:
file.write(f"{name}={value}\n")
stdout.flush()
def get_ld_library_path(prefix: Path) -> str:
# given a prefix, the ld library path can be found at
# <prefix>/lib/* or sometimes just <prefix>/lib
# this function returns the path to the ld library path
# first, check if the ld library path exists at <prefix>/lib/*
ld_library_paths = list(prefix.glob("lib/*"))
if len(ld_library_paths) == 1:
return ld_library_paths[0].absolute().as_posix()
# if the ld library path does not exist at <prefix>/lib/*,
# return <prefix>/lib
ld_library_path = prefix / "lib"
if ld_library_path.exists():
return ld_library_path.absolute().as_posix()
return ""
def main():
if sys.platform == "win32":
logger.info("Skipping build on windows")
return
with tempfile.TemporaryDirectory() as tmpdir:
with gha_group("Downloading and Extracting Cairo"):
logger.info(f"Downloading cairo version {CAIRO_VERSION}")
download_file(CAIRO_URL, os.path.join(tmpdir, "cairo.tar.xz"))
logger.info("Downloading cairo sha256sum")
download_file(CAIRO_SHA256_URL, os.path.join(tmpdir, "cairo.sha256sum"))
logger.info("Verifying cairo sha256sum")
with open(os.path.join(tmpdir, "cairo.sha256sum")) as file:
sha256sum = file.read().split()[0]
verify_sha256sum(os.path.join(tmpdir, "cairo.tar.xz"), sha256sum)
logger.info("Extracting cairo")
extract_tar_xz(os.path.join(tmpdir, "cairo.tar.xz"), tmpdir)
with gha_group("Installing meson and ninja"):
logger.info("Creating virtual environment")
run_command([sys.executable, "-m", "venv", os.path.join(tmpdir, VENV_NAME)])
logger.info("Installing meson and ninja")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "pip"),
"install",
"meson",
"ninja",
]
)
env_vars = {
# add the venv bin directory to PATH so that meson can find ninja
"PATH": f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{os.environ['PATH']}",
}
with gha_group("Building and Installing Cairo"):
logger.info("Running meson setup")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"setup",
BUILD_DIR,
f"--prefix={INSTALL_PREFIX.absolute().as_posix()}",
"--buildtype=release",
"-Dtests=disabled",
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info("Running meson compile")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"compile",
"-C",
BUILD_DIR,
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info("Running meson install")
run_command(
[
os.path.join(tmpdir, VENV_NAME, "bin", "meson"),
"install",
"-C",
BUILD_DIR,
],
cwd=os.path.join(tmpdir, f"cairo-{CAIRO_VERSION}"),
env=env_vars,
)
logger.info(f"Successfully built cairo and installed it to {INSTALL_PREFIX}")
if __name__ == "__main__":
if "--set-env-vars" in sys.argv:
with gha_group("Setting environment variables"):
# append the pkgconfig directory to PKG_CONFIG_PATH
set_env_var_gha(
"PKG_CONFIG_PATH",
f"{Path(get_ld_library_path(INSTALL_PREFIX), 'pkgconfig').as_posix()}{os.pathsep}"
f'{os.getenv("PKG_CONFIG_PATH", "")}',
)
set_env_var_gha(
"LD_LIBRARY_PATH",
f"{get_ld_library_path(INSTALL_PREFIX)}{os.pathsep}"
f'{os.getenv("LD_LIBRARY_PATH", "")}',
)
sys.exit(0)
main()
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ${{ matrix.os }}
env:
DISPLAY: :0
PYTEST_ADDOPTS: "--color=yes" # colors in pytest
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-latest, windows-latest]
python: ["3.9", "3.10", "3.11", "3.12"]
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Poetry
run: |
pipx install "poetry==1.7.*"
poetry config virtualenvs.prefer-active-python true
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: "poetry"
- name: Setup macOS PATH
if: runner.os == 'macOS'
run: |
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Setup cache variables
shell: bash
id: cache-vars
run: |
echo "date=$(/bin/date -u "+%m%w%Y")" >> $GITHUB_OUTPUT
- name: Install and cache ffmpeg (all OS)
uses: FedericoCarboni/setup-ffmpeg@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: setup-ffmpeg
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: python3-opengl libpango1.0-dev xvfb freeglut3-dev
version: 1.0
- name: Install Texlive (Linux)
if: runner.os == 'Linux'
uses: teatimeguest/setup-texlive-action@v3
with:
cache: true
packages: scheme-basic fontspec inputenc fontenc tipa mathrsfs calligra xcolor standalone preview doublestroke ms everysel setspace rsfs relsize ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex
- name: Start virtual display (Linux)
if: runner.os == 'Linux'
run: |
# start xvfb in background
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
- name: Setup Cairo Cache
uses: actions/cache@v3
id: cache-cairo
if: runner.os == 'Linux' || runner.os == 'macOS'
with:
path: ${{ github.workspace }}/third_party
key: ${{ runner.os }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}
- name: Build and install Cairo (Linux and macOS)
if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-cairo.outputs.cache-hit != 'true'
run: python .github/scripts/ci_build_cairo.py
- name: Set env vars for Cairo (Linux and macOS)
if: runner.os == 'Linux' || runner.os == 'macOS'
run: python .github/scripts/ci_build_cairo.py --set-env-vars
- name: Setup macOS cache
uses: actions/cache@v3
id: cache-macos
if: runner.os == 'macOS'
with:
path: ${{ github.workspace }}/macos-cache
key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1
- name: Install system dependencies (MacOS)
if: runner.os == 'macOS' && steps.cache-macos.outputs.cache-hit != 'true'
run: |
tinyTexPackages=$(python -c "import json;print(' '.join(json.load(open('.github/manimdependency.json'))['macos']['tinytex']))")
IFS=' '
read -a ttp <<< "$tinyTexPackages"
oriPath=$PATH
sudo mkdir -p $PWD/macos-cache
echo "Install TinyTeX"
sudo curl -L -o "/tmp/TinyTeX.tgz" "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.tgz"
sudo tar zxf "/tmp/TinyTeX.tgz" -C "$PWD/macos-cache"
export PATH="$PWD/macos-cache/TinyTeX/bin/universal-darwin:$PATH"
sudo tlmgr update --self
for i in "${ttp[@]}"; do
sudo tlmgr install "$i"
done
export PATH="$oriPath"
echo "Completed TinyTeX"
- name: Add macOS dependencies to PATH
if: runner.os == 'macOS'
shell: bash
run: |
echo "/Library/TeX/texbin" >> $GITHUB_PATH
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
echo "$PWD/macos-cache/TinyTeX/bin/universal-darwin" >> $GITHUB_PATH
- name: Setup Windows cache
id: cache-windows
if: runner.os == 'Windows'
uses: actions/cache@v3
with:
path: ${{ github.workspace }}\ManimCache
key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1
- uses: ssciwr/setup-mesa-dist-win@v1
- name: Install system dependencies (Windows)
if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'
run: |
$tinyTexPackages = $(python -c "import json;print(' '.join(json.load(open('.github/manimdependency.json'))['windows']['tinytex']))") -Split ' '
$OriPath = $env:PATH
echo "Install Tinytex"
Invoke-WebRequest "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.zip" -OutFile "$($env:TMP)\TinyTex.zip"
Expand-Archive -LiteralPath "$($env:TMP)\TinyTex.zip" -DestinationPath "$($PWD)\ManimCache\LatexWindows"
$env:Path = "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows;$($env:PATH)"
tlmgr update --self
foreach ($c in $tinyTexPackages){
$c=$c.Trim()
tlmgr install $c
}
$env:PATH=$OriPath
echo "Completed Latex"
- name: Add Windows dependencies to PATH
if: runner.os == 'Windows'
run: |
$env:Path += ";" + "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows"
$env:Path = "$env:USERPROFILE\.poetry\bin;$($env:PATH)"
echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install manim-physics
run: |
poetry config installer.modern-installation false
poetry install --with dev
- name: Run tests
run: |
poetry run python -m pytest
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
*.log
# Sphinx documentation
docs/_build/
docs/build/
docs/source/_autosummary/
docs/source/reference/
docs/source/_build/
rendering_times.csv
# PyBuilder
target/
# Jupyter Notebook
jupyter/
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# PyCharm
/.idea/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
*.pyc
*.bak
.DS_Store
.floo
.flooignore
.vscode
.vs
*.xml
*.iml
media
.eggs/
build/
dist/
/media_dir.txt
# ^TODO: Remove the need for this with a proper config file
================================================
FILE: .pre-commit-config.yaml
================================================
default_stages: [commit, push]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: mixed-line-ending
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/asottile/blacken-docs
rev: 1.13.0
hooks:
- id: blacken-docs
additional_dependencies:
- black==23.3.0
- repo: https://github.com/codespell-project/codespell
rev: v2.2.4
hooks:
- id: codespell
================================================
FILE: .readthedocs.yml
================================================
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
apt_packages:
- libpango1.0-dev
- ffmpeg
- graphviz
python:
install:
- requirements: docs/rtd-requirements.txt
- requirements: docs/requirements.txt
- method: pip
path: .
================================================
FILE: README.md
================================================
# manim-physics
## Introduction
This is a 2D physics simulation plugin that allows you to generate complicated
scenes in various branches of Physics such as rigid mechanics,
electromagnetism, wave etc. **Due to some reason, I (Matheart) may not have
time to maintain this repo, if you want to contribute please seek help from
other contributors.**
Official Documentation: https://manim-physics.readthedocs.io/en/latest/
Contributors:
- [**pdcxs**](https://github.com/pdcxs)
- [**Matheart**](https://github.com/Matheart)
- [**icedcoffeeee**](https://github.com/icedcoffeeee)
# Installation
`manim-physics` is a package on pypi, and can be directly installed using pip:
```bash
pip install manim-physics
```
================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
# Path base is the source directory
SOURCEDIR = .
BUILDDIR = ../build
# Put it first so that "make" without argument is like "make help".
help:
@(cd source; $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O))
.PHONY: help Makefile i18n
# All the code is executed as if everything was launched in one shell.
.ONESHELL:
# Like make clean but also remove files generated by autosummary and
# rendered videos.
cleanall: clean
@rm source/reference/*
@rm -rf source/media
@rm -f rendering_times.csv
i18n:
@(cd source; $(SPHINXBUILD) -M gettext "$(SOURCEDIR)" ../i18n/ -t skip-manim $(SPHINXOPTS) $(O);cd ../i18n;bash stripUntranslatable.sh)
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@(cd source; $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O))
================================================
FILE: docs/make.bat
================================================
@ECHO OFF
pushd %~dp0\source
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
REM The paths are taken from the source directory
set SOURCEDIR=.
set BUILDDIR=..\build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
================================================
FILE: docs/requirements.txt
================================================
furo
myst-parser
sphinx<5.1
sphinx-copybutton
sphinxext-opengraph
================================================
FILE: docs/rtd-requirements.txt
================================================
jupyterlab
sphinxcontrib-programoutput
================================================
FILE: docs/source/_static/custom.css
================================================
@media (prefers-color-scheme: dark) {
span.nc {
text-decoration: none !important;
}
}
.admonition-manim-example {
padding: 0;
display: flex;
flex-direction: column;
}
.admonition-manim-example p.admonition-title {
font-weight: 600;
font-size: 0.925rem;
margin: 0;
}
.admonition-manim-example .highlight-python {
margin: 0;
}
.admonition-manim-example .highlight {
border-radius: 0;
}
.admonition-manim-example .highlight pre {
font-size: 15px;
}
.manim-video {
width: 100%;
padding: 8px 0;
outline: 0;
}
.admonition-manim-example .manim-video {
padding: 0;
}
.admonition-manim-example img {
margin-bottom: 0;
}
.admonition-manim-example p:last-child {
margin-top: 0;
padding-left: 0.5rem;
padding-bottom: 0.15rem;
font-size: 15px;
}
.admonition-manim-example .copybtn {
margin-right: 6px;
font-size: 18px;
}
.admonition-manim-example .copybtn:hover {
cursor: pointer;
}
p.rubric{
text-transform: capitalize;
font-size: 1.25rem;
font-weight: bold;
}
.sig-param{
color: var(--color-content-foreground);
}
dl.c .field-list dt, dl.cpp .field-list dt, dl.js .field-list dt, dl.py .field-list dt {
text-transform: capitalize;
font-weight: bold;
font-size: var(--font-size--normal);
}
h4, h5, h6{
text-transform: none;
}
/* yikes-ish attempt at bugfix for navbar on some browsers */
.sidebar-tree a.internal.reference {
display: table-cell;
}
================================================
FILE: docs/source/_static/responsiveSvg.js
================================================
window.addEventListener("load", function () {
const styleElements = []
const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
const diagrams = document.querySelectorAll("object.inheritance.graphviz");
for (let diagram of diagrams) {
style = document.createElement('style');
styleElements.push(style);
console.log(diagram);
diagram.contentDocument.firstElementChild.appendChild(style);
}
function setColorScheme(e) {
let colors, additions = "";
if (e.matches) {
// Dark
colors = {
text: "#e07a5f",
box: "#383838",
edge: "#d0d0d0",
background: "#131416"
};
} else {
// Light
colors = {
text: "#e07a5f",
box: "#fff",
edge: "#413c3c",
background: "#ffffff"
};
additions = `
.node polygon {
filter: drop-shadow(0 1px 3px #0002);
}
`
}
for (let style of styleElements) {
style.innerHTML = `
svg {
background-color: ${colors.background};
}
.node text {
fill: ${colors.text};
}
.node polygon {
fill: ${colors.box};
}
.edge polygon {
fill: ${colors.edge};
stroke: ${colors.edge};
}
.edge path {
stroke: ${colors.edge};
}
${additions}
`;
}
}
setColorScheme(colorSchemeQuery);
colorSchemeQuery.addEventListener("change", setColorScheme);
});
================================================
FILE: docs/source/_templates/autosummary/class.rst
================================================
{{ name | escape | underline}}
Qualified name: ``{{ fullname | escape }}``
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
:show-inheritance:
:members:
{% block methods %}
{%- if methods %}
.. rubric:: {{ _('Methods') }}
.. autosummary::
:nosignatures:
{% for item in methods if item != '__init__' and item not in inherited_members %}
~{{ name }}.{{ item }}
{%- endfor %}
{%- endif %}
{%- endblock %}
{% block attributes %}
{%- if attributes %}
.. rubric:: {{ _('Attributes') }}
.. autosummary::
{% for item in attributes %}
~{{ name }}.{{ item }}
{%- endfor %}
{%- endif %}
{% endblock %}
================================================
FILE: docs/source/_templates/autosummary/module.rst
================================================
{{ name | escape | underline }}
.. currentmodule:: {{ fullname }}
.. automodule:: {{ fullname }}
{% block attributes %}
{% if attributes %}
.. rubric:: Module Attributes
.. autosummary::
{% for item in attributes %}
{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
{% block classes %}
{% if classes %}
.. rubric:: Classes
.. autosummary::
:toctree: .
:nosignatures:
{% for class in classes %}
{{ class }}
{% endfor %}
{% endif %}
{% endblock %}
{% block functions %}
{% if functions %}
.. rubric:: {{ _('Functions') }}
{% for item in functions %}
.. autofunction:: {{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
{% block exceptions %}
{% if exceptions %}
.. rubric:: {{ _('Exceptions') }}
.. autosummary::
{% for item in exceptions %}
{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
{% block modules %}
{% if modules %}
.. rubric:: Modules
.. autosummary::
:toctree:
:recursive:
{% for item in modules %}
{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
================================================
FILE: docs/source/changelog.rst
================================================
=========
Changelog
=========
**v0.4.1**
==========
Bugfix
------
- `#34 <https://github.com/Matheart/manim-physics/pull/35>`_ : Magnetic fields
now accept multiple wires
**v0.4.0**
==========
Breaking Changes
----------------
- Supported Python versions include 3.9 to 3.12
- Updated manim version
- Updated dependency versions
**v0.3.0**
==========
Breaking Changes
----------------
- Huge library refactor.
- :class:`~.MagneticField` now takes a :class:`~.Wire` parameter. This allows
for a 3D field.
- Optimized field functions for both :class:`~.ElectricField` and
:class:`~.MagneticField`.
**v0.2.5**
==========
Bugfixes
--------
- ``VGroup`` s can be whole rigid bodies. Support for ``SVGMobject`` s
**v0.2.4**
==========
2021.12.25
New Features
------------
- Hosted `official documentation
<https://manim-physics.readthedocs.io/en/latest/>`_ on
readthedocs. The readme might be restructured due to redundancy.
- New ``lensing`` module: Mobjects including ``Lens`` and ``Ray``
- ``SpaceScene`` can now specify the gravity vector.
- Fixed ``ConvertToOpenGL`` import error for ``manim v0.15.0``.
Improvements
-------------
- Combined ``BarMagneticField`` with ``CurrentMagneticField`` into
``MagneticField``.
- Improved the updaters for ``pendulum`` module. Frame rate won't show any
lagging in the pendulum rods.
Bugfixes
---------
- Updated deprecated parameters in the ``wave`` module.
**v0.2.3**
==========
2021.07.14
Bugfixes
--------
- Fix the small arrow bug in ``ElectricField``
**v0.2.2**
==========
2021.07.06
New objects
-----------
- **Rigid Mechanics**: Pendulum
Bugfixes
--------
- Fix the ``__all__`` bug, now ``rigid_mechanics.py`` can run normally.
Improvements
------------
- Rewrite README.md to improve its readability
**v0.2.1**
==========
2021.07.03
New objects
-----------
- **Electromagnetism**: Charge, ElectricField, Current, CurrentMagneticField,
BarMagnet, and BarMagnetField
- **Wave**: LinearWave, RadialWave, StandingWave
Bugfixes
--------
- Fix typo
Improvements
------------
- Simplify rigid-mechanics
**v0.2.0**
==========
2021.07.01
Breaking Changes
----------------
- Objects in the manim-physics plugin are classified into several **main
branches** including rigid mechanics simulation, electromagnetism and wave.
================================================
FILE: docs/source/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
from __future__ import annotations
import os
import sys
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("."))
# -- Project information -----------------------------------------------------
project = "Manim Physics"
copyright = "2020-2024, The Manim Physics Dev Team"
author = "The Manim Physics Dev Team"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx_copybutton",
"sphinx.ext.napoleon",
"sphinx.ext.autosummary",
"sphinx.ext.doctest",
"sphinx.ext.extlinks",
"sphinx.ext.viewcode",
"manim.utils.docbuild.manim_directive",
"sphinxcontrib.programoutput",
"myst_parser",
]
# Automatically generate stub pages when using the .. autosummary directive
autosummary_generate = True
# generate documentation from type hints
autodoc_typehints = "description"
autoclass_content = "both"
# controls whether functions documented by the autofunction directive
# appear with their full module names
add_module_names = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# Custom section headings in our documentation
napoleon_custom_sections = ["Tests", ("Test", "Tests")]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns: list[str] = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "furo"
# html_favicon = str(Path("_static/favicon.ico"))
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]
html_theme_options = {
"source_repository": "https://github.com/Matheart/manim-physics/",
"source_branch": "main",
"source_directory": "docs/source/",
"top_of_page_button": None,
"light_css_variables": {
"color-content-foreground": "#000000",
"color-background-primary": "#ffffff",
"color-background-border": "#ffffff",
"color-sidebar-background": "#f8f9fb",
"color-brand-content": "#1c00e3",
"color-brand-primary": "#192bd0",
"color-link": "#c93434",
"color-link--hover": "#5b0000",
"color-inline-code-background": "#f6f6f6;",
"color-foreground-secondary": "#000",
},
"dark_css_variables": {
"color-content-foreground": "#ffffffd9",
"color-background-primary": "#131416",
"color-background-border": "#303335",
"color-sidebar-background": "#1a1c1e",
"color-brand-content": "#2196f3",
"color-brand-primary": "#007fff",
"color-link": "#51ba86",
"color-link--hover": "#9cefc6",
"color-inline-code-background": "#262626",
"color-foreground-secondary": "#ffffffd9",
},
}
html_title = f"Manim Physics v0.4.0"
# This specifies any additional css files that will override the theme's
html_css_files = ["custom.css"]
# external links
extlinks = {
"issue": ("https://github.com/Matheart/manim-physics/issues/%s", "#%s"),
"pr": ("https://github.com/Matheart/manim-physics/pull/%s", "#%s"),
}
# opengraph settings
ogp_site_name = "Manim Physics | Documentation"
html_js_files = [
"responsiveSvg.js",
]
================================================
FILE: docs/source/contributing.rst
================================================
Contributing
============
Contributions are welcome! The repository owner (Matheart) might not be
available, any pull requests or issues will be attended by other developers.
There's three parts in a contribution:
1. The proposed addition/improvement.
2. Documentation for the new addition.
3. Tests for the addition.
Nearly all contributing guidelines here are identical to `Manim's Contributing
Guidelines <https://docs.manim.community/en/stable/contributing.html>`_.
================================================
FILE: docs/source/index.rst
================================================
Manim Physics
-------------
Welcome to the `manim-physics` official documentation!
Installation
++++++++++++
``manim-physics`` is a package on pypi, and can be directly installed using
pip:
.. code-block:: powershell
pip install manim-physics
.. warning::
Please do not directly clone the github repo! The repo is still under
development and it is not a stable version, download manim-physics through
pypi.
Usage
+++++
Include the import at the top of the .py file
.. code-block::
from manim_physics import *
Index
+++++
.. toctree::
:maxdepth: 2
reference
changelog
contributing
================================================
FILE: docs/source/reference.rst
================================================
Reference
---------
.. toctree::
:maxdepth: 2
reference_index/electromagnetism
reference_index/optics
reference_index/rigid_mechanics
reference_index/wave
================================================
FILE: docs/source/reference_index/electromagnetism.rst
================================================
Electromagnetism
================
.. currentmodule:: manim_physics
.. autosummary::
:toctree: ../reference
~electromagnetism.electrostatics
~electromagnetism.magnetostatics
================================================
FILE: docs/source/reference_index/optics.rst
================================================
Optics
------
.. currentmodule:: manim_physics
.. autosummary::
:toctree: ../reference
~optics.lenses
~optics.rays
================================================
FILE: docs/source/reference_index/rigid_mechanics.rst
================================================
Rigid Mechanics
---------------
.. currentmodule:: manim_physics
.. autosummary::
:toctree: ../reference
~rigid_mechanics.rigid_mechanics
~rigid_mechanics.pendulum
================================================
FILE: docs/source/reference_index/wave.rst
================================================
Waves
========
.. currentmodule:: manim_physics
.. autosummary::
:toctree: ../reference
~wave
================================================
FILE: example.py
================================================
from manim_physics import *
class MagneticFieldExample(ThreeDScene):
def construct(self):
wire = Wire(Circle(2).rotate(PI / 2, UP))
mag_field = MagneticField(wire)
self.set_camera_orientation(PI / 3, PI / 4)
self.add(wire, mag_field)
================================================
FILE: manim_physics/__init__.py
================================================
__version__ = "0.2.3"
from manim import *
from .electromagnetism.electrostatics import *
from .electromagnetism.magnetostatics import *
from .optics.lenses import *
from .optics.rays import *
from .rigid_mechanics.pendulum import *
from .rigid_mechanics.rigid_mechanics import *
from .wave import *
================================================
FILE: manim_physics/electromagnetism/__init__.py
================================================
================================================
FILE: manim_physics/electromagnetism/electrostatics.py
================================================
"""Electrostatics module"""
from __future__ import annotations
from typing import Iterable
from manim import normalize
from manim.constants import ORIGIN, TAU
from manim.mobject.geometry.arc import Arc, Dot
from manim.mobject.geometry.polygram import Rectangle
from manim.mobject.types.vectorized_mobject import VGroup
from manim.mobject.vector_field import ArrowVectorField
from manim.utils.color import BLUE, RED, RED_A, RED_D, color_gradient
import numpy as np
__all__ = [
"Charge",
"ElectricField",
]
class Charge(VGroup):
def __init__(
self,
magnitude: float = 1,
point: np.ndarray = ORIGIN,
add_glow: bool = True,
**kwargs,
) -> None:
"""An electrostatic charge object to produce an :class:`~ElectricField`.
Parameters
----------
magnitude
The strength of the electrostatic charge.
point
The position of the charge.
add_glow
Whether to add a glowing effect. Adds rings of
varying opacities to simulate glowing effect.
kwargs
Additional parameters to be passed to ``VGroup``.
"""
VGroup.__init__(self, **kwargs)
self.magnitude = magnitude
self.point = point
self.radius = (abs(magnitude) * 0.4 if abs(magnitude) < 2 else 0.8) * 0.3
if magnitude > 0:
label = VGroup(
Rectangle(width=0.32 * 1.1, height=0.006 * 1.1).set_z_index(1),
Rectangle(width=0.006 * 1.1, height=0.32 * 1.1).set_z_index(1),
)
color = RED
layer_colors = [RED_D, RED_A]
layer_radius = 4
else:
label = Rectangle(width=0.27, height=0.003)
color = BLUE
layer_colors = ["#3399FF", "#66B2FF"]
layer_radius = 2
if add_glow: # use many arcs to simulate glowing
layer_num = 80
color_list = color_gradient(layer_colors, layer_num)
opacity_func = lambda t: 1500 * (1 - abs(t - 0.009) ** 0.0001)
rate_func = lambda t: t**2
for i in range(layer_num):
self.add(
Arc(
radius=layer_radius * rate_func((0.5 + i) / layer_num),
angle=TAU,
color=color_list[i],
stroke_width=101
* (rate_func((i + 1) / layer_num) - rate_func(i / layer_num))
* layer_radius,
stroke_opacity=opacity_func(rate_func(i / layer_num)),
).shift(point)
)
self.add(Dot(point=self.point, radius=self.radius, color=color))
self.add(label.scale(self.radius / 0.3).shift(point))
for mob in self:
mob.set_z_index(1)
class ElectricField(ArrowVectorField):
def __init__(self, *charges: Charge, **kwargs) -> None:
"""An electric field.
Parameters
----------
charges
The charges affecting the electric field.
kwargs
Additional parameters to be passed to ``ArrowVectorField``.
Examples
--------
.. manim:: ElectricFieldExampleScene
:save_last_frame:
from manim_physics import *
class ElectricFieldExampleScene(Scene):
def construct(self):
charge1 = Charge(-1, LEFT + DOWN)
charge2 = Charge(2, RIGHT + DOWN)
charge3 = Charge(-1, UP)
field = ElectricField(charge1, charge2, charge3)
self.add(charge1, charge2, charge3)
self.add(field)
"""
self.charges = charges
positions = []
magnitudes = []
for charge in charges:
positions.append(charge.get_center())
magnitudes.append(charge.magnitude)
super().__init__(lambda p: self._field_func(p, positions, magnitudes), **kwargs)
def _field_func(
self,
p: np.ndarray,
positions: Iterable[np.ndarray],
magnitudes: Iterable[float],
) -> np.ndarray:
field_vect = np.zeros(3)
for p0, mag in zip(positions, magnitudes):
r = p - p0
dist = np.linalg.norm(r)
if dist < 0.1:
return np.zeros(3)
field_vect += mag / dist**2 * normalize(r)
return field_vect
================================================
FILE: manim_physics/electromagnetism/magnetostatics.py
================================================
"""Magnetostatics module"""
from __future__ import annotations
import itertools as it
from typing import Iterable, Tuple
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
from manim.mobject.vector_field import ArrowVectorField
import numpy as np
__all__ = ["Wire", "MagneticField"]
class Wire(VMobject, metaclass=ConvertToOpenGL):
"""An abstract class denoting a current carrying wire to produce a
:class:`~MagneticField`.
Parameters
----------
stroke
The original wire ``VMobject``. The resulting wire takes its form.
current
The magnitude of current flowing in the wire.
samples
The number of segments of the wire used to create the
:class:`~MagneticField`.
kwargs
Additional parameters passed to ``VMobject``.
.. note::
See :class:`~MagneticField` for examples.
"""
def __init__(
self,
stroke: VMobject,
current: float = 1,
samples: int = 16,
**kwargs,
):
self.current = current
self.samples = samples
super().__init__(**kwargs)
self.set_points(stroke.points)
class MagneticField(ArrowVectorField):
"""A magnetic field.
Parameters
----------
wires
All wires contributing to the total field.
kwargs
Additional parameters to be passed to ``ArrowVectorField``.
Example
-------
.. manim:: MagneticFieldExample
:save_last_frame:
from manim_physics import *
class MagneticFieldExample(ThreeDScene):
def construct(self):
wire = Wire(Circle(2).rotate(PI / 2, UP))
mag_field = MagneticField(
wire,
x_range=[-4, 4],
y_range=[-4, 4],
)
self.set_camera_orientation(PI / 3, PI / 4)
self.add(wire, mag_field)
"""
def __init__(self, *wires: Wire, **kwargs):
dls = []
currents = []
for wire in wires:
points = [
wire.point_from_proportion(i)
for i in np.linspace(0, 1, wire.samples + 1)
]
dls.append(list(zip(points, points[1:])))
currents.append(wire.current)
super().__init__(
lambda p: MagneticField._field_func(p, dls, currents), **kwargs
)
@staticmethod
def _field_func(
p: np.ndarray,
dls: Iterable[Tuple[np.ndarray, np.ndarray]],
currents: Iterable[float],
):
B_field = np.zeros(3)
for dl in dls:
for (r0, r1), I in it.product(dl, currents):
dr = r1 - r0
r = p - r0
dist = np.linalg.norm(r)
if dist < 0.1:
return np.zeros(3)
B_field += np.cross(dr, r) * I / dist**4
return B_field
================================================
FILE: manim_physics/optics/__init__.py
================================================
"""A lensing module.
Currently only shows refraction in lenses and not
total internal reflection.
"""
================================================
FILE: manim_physics/optics/lenses.py
================================================
"""Lenses for refracting Rays.
"""
from __future__ import annotations
from typing import Iterable, Tuple
from manim import config
from manim.constants import LEFT, RIGHT
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.boolean_ops import Difference, Intersection
from manim.mobject.geometry.polygram import Square
from manim.mobject.types.vectorized_mobject import VMobject, VectorizedPoint
import numpy as np
from shapely import geometry as gm
__all__ = ["Lens"]
try:
# For manim < 0.15.0
from manim.mobject.opengl_compatibility import ConvertToOpenGL
except ModuleNotFoundError:
# For manim >= 0.15.0
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
def intersection(vmob1: VMobject, vmob2: VMobject) -> Iterable[Iterable[float]]:
"""intersection points of 2 curves"""
a = gm.LineString(vmob1.points)
b = gm.LineString(vmob2.points)
intersects: gm.GeometryCollection = a.intersection(b)
try: # for intersections > 1
return np.array(
[[[x, y, z] for x, y, z in m.coords][0] for m in intersects.geoms]
)
except: # else
return np.array([[x, y, z] for x, y, z in intersects.coords])
def snell(i_ang: float, n: float) -> float:
"""accepts radians, returns radians"""
return np.arcsin(np.sin(i_ang) / n)
def antisnell(r_ang: float, n: float) -> float:
"""accepts radians, returns radians"""
return np.arcsin(np.sin(r_ang) * n)
class Lens(VMobject, metaclass=ConvertToOpenGL):
def __init__(self, f: float, d: float, n: float = 1.52, **kwargs) -> None:
"""A lens. Commonly used with :class:`~Ray` .
Parameters
----------
f
Focal length. This does not correspond correctly
to the point of focus (Known issue). Positive f
returns a convex lens, negative for concave.
d
Lens thickness
n
Refractive index. By default, glass.
kwargs
Additional parameters to be passed to :class:`~VMobject` .
"""
super().__init__(**kwargs)
self.f = f
f *= 50 / 7 * f if f > 0 else -50 / 7 * f # this is odd, but it works
if f > 0:
r = ((n - 1) ** 2 * f * d / n) ** 0.5
else:
r = ((n - 1) ** 2 * -f * d / n) ** 0.5
self.d = d
self.n = n
self.r = r
if f > 0:
self.set_points(
Intersection(
a := Circle(r).shift(RIGHT * (r - d / 2)),
b := Circle(r).shift(LEFT * (r - d / 2)),
)
.insert_n_curves(50)
.points
)
else:
self.set_points(
Difference(
Difference(
Square(2 * 0.7 * r),
a := Circle(r).shift(LEFT * (r + d / 2)),
),
b := Circle(r).shift(RIGHT * (r + d / 2)),
)
.insert_n_curves(50)
.points
)
self.add(VectorizedPoint(a.get_center()), VectorizedPoint(b.get_center()))
@property
def C(self) -> Tuple[Iterable[float]]:
"""Returns a tuple of two points corresponding to the centers of curvature."""
i = 0
i += 1 if config.renderer != "opengl" else 0
return self[i].points[0], self[i + 1].points[0] # why is this confusing
================================================
FILE: manim_physics/optics/rays.py
================================================
"""Rays of light. Refracted by Lenses."""
from __future__ import annotations
from typing import Iterable
from manim import config
from manim.mobject.geometry.line import Line
from manim.utils.space_ops import angle_of_vector, rotate_vector
import numpy as np
from .lenses import Lens, antisnell, intersection, snell
__all__ = [
"Ray",
]
class Ray(Line):
def __init__(
self,
start: Iterable[float],
direction: Iterable[float],
init_length: float = 5,
propagate: Iterable[Lens] | None = None,
**kwargs,
) -> None:
"""A light ray.
Parameters
----------
start
The start point of the ray
direction
The direction of the ray
init_length
The initial length of the ray. Once propagated,
the length are lengthened to showcase lensing.
propagate
A list of lenses to propagate through.
Example
-------
.. manim:: RayExampleScene
:save_last_frame:
from manim_physics import *
class RayExampleScene(Scene):
def construct(self):
lens_style = {"fill_opacity": 0.5, "color": BLUE}
a = Lens(-5, 1, **lens_style).shift(LEFT)
a2 = Lens(5, 1, **lens_style).shift(RIGHT)
b = [
Ray(LEFT * 5 + UP * i, RIGHT, 8, [a, a2], color=RED)
for i in np.linspace(-2, 2, 10)
]
self.add(a, a2, *b)
"""
self.init_length = init_length
self.propagated = False
super().__init__(start, start + direction * init_length, **kwargs)
if propagate:
self.propagate(*propagate)
def propagate(self, *lenses: Lens) -> None:
"""Let the ray propagate through the list
of lenses passed.
Parameters
----------
lenses
All the lenses for the ray to propagate through
"""
# TODO: make modular(?) Clean up logic
sorted_lens = self._sort_lens(lenses)
for lens in sorted_lens:
intersects = intersection(lens, self)
if len(intersects) == 0:
continue
intersects = self._sort_intersections(intersects)
if not self.propagated:
self.put_start_and_end_on(
self.start,
intersects[1],
)
else:
nppcc = (
self.n_points_per_cubic_curve
if config.renderer != "opengl"
else self.n_points_per_curve
)
self.points = self.points[:-nppcc]
self.add_line_to(intersects[1])
self.end = intersects[1]
i_ang = angle_of_vector(self.end - lens.C[0])
i_ang -= angle_of_vector(self.start - self.end)
r_ang = snell(i_ang, lens.n)
r_ang *= -1 if lens.f > 0 else 1
ref_ray = rotate_vector(lens.C[0] - self.end, r_ang)
intersects = intersection(
lens,
Line(
self.end - ref_ray * self.init_length,
self.end + ref_ray * self.init_length,
),
)
intersects = self._sort_intersections(intersects)
self.add_line_to(intersects[1])
self.start = self.end
self.end = intersects[1]
i_ang = angle_of_vector(self.end - lens.C[1])
i_ang -= angle_of_vector(self.start - self.end)
if np.abs(np.sin(i_ang)) < 1 / lens.n:
r_ang = antisnell(i_ang, lens.n)
r_ang *= -1 if lens.f < 0 else 1
ref_ray = rotate_vector(lens.C[1] - self.end, r_ang)
ref_ray *= -1 if lens.f > 0 else 1
self.add_line_to(self.end + ref_ray * self.init_length)
self.start = self.end
self.end = self.get_end()
self.propagated = True
def _sort_lens(self, lenses: Iterable[Lens]) -> Iterable[Lens]:
dists = []
for lens in lenses:
try:
dists += [
[np.linalg.norm(intersection(self, lens)[0] - self.start), lens]
]
except:
dists += [[np.inf, lens]]
dists.sort(key=lambda x: x[0])
return np.array(dists, dtype=object)[:, 1]
def _sort_intersections(
self, intersections: Iterable[Iterable[float]]
) -> Iterable[Iterable[float]]:
result = []
for inters in intersections:
result.append([np.linalg.norm(inters - self.end), inters])
result.sort(key=lambda x: x[0])
return np.array(result, dtype=object)[:, 1]
================================================
FILE: manim_physics/rigid_mechanics/__init__.py
================================================
================================================
FILE: manim_physics/rigid_mechanics/pendulum.py
================================================
r"""Pendulums.
:class:`~MultiPendulum` and :class:`~Pendulum` both stem from the
:py:mod:`~rigid_mechanics` feature.
"""
from __future__ import annotations
from typing import Iterable
from manim.constants import DOWN, RIGHT, UP
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.line import Line
from manim.mobject.mobject import Mobject
from manim.mobject.types.vectorized_mobject import VGroup
from manim.utils.color import ORANGE
import numpy as np
import pymunk
from .rigid_mechanics import SpaceScene
__all__ = [
"Pendulum",
"MultiPendulum",
"SpaceScene",
]
class MultiPendulum(VGroup):
def __init__(
self,
*bobs: Iterable[np.ndarray],
pivot_point: np.ndarray = UP * 2,
rod_style: dict = {},
bob_style: dict = {
"radius": 0.1,
"color": ORANGE,
"fill_opacity": 1,
},
**kwargs,
) -> None:
"""A multipendulum.
Parameters
----------
bobs
Positions of pendulum bobs.
pivot_point
Position of the pivot.
rod_style
Parameters for ``Line``.
bob_style
Parameters for ``Circle``.
kwargs
Additional parameters for ``VGroup``.
Examples
--------
.. manim:: MultiPendulumExample
:quality: low
from manim_physics import *
class MultiPendulumExample(SpaceScene):
def construct(self):
p = MultiPendulum(RIGHT, LEFT)
self.add(p)
self.make_rigid_body(*p.bobs)
p.start_swinging()
self.add(TracedPath(p.bobs[-1].get_center, stroke_color=BLUE))
self.wait(10)
"""
self.pivot_point = pivot_point
self.bobs = VGroup(*[Circle(**bob_style).move_to(i) for i in bobs])
self.pins = [pivot_point]
self.pins += bobs
self.rods = VGroup()
self.rods += Line(self.pivot_point, self.bobs[0].get_center(), **rod_style)
self.rods.add(
*(
Line(
self.bobs[i].get_center(),
self.bobs[i + 1].get_center(),
**rod_style,
)
for i in range(len(bobs) - 1)
)
)
super().__init__(**kwargs)
self.add(self.rods, self.bobs)
def _make_joints(
self, mob1: Mobject, mob2: Mobject, spacescene: SpaceScene
) -> None:
a = mob1.body
if type(mob2) == np.ndarray:
b = pymunk.Body(body_type=pymunk.Body.STATIC)
b.position = mob2[0], mob2[1]
else:
b = mob2.body
joint = pymunk.PinJoint(a, b)
spacescene.space.space.add(joint)
def _redraw_rods(self, mob: Line, pins, i):
try:
x, y, _ = pins[i]
except:
x, y = pins[i].body.position
x1, y1 = pins[i + 1].body.position
mob.put_start_and_end_on(
RIGHT * x + UP * y,
RIGHT * x1 + UP * y1,
)
def start_swinging(self) -> None:
"""Start swinging."""
spacescene: SpaceScene = self.bobs[0].spacescene
pins = [self.pivot_point]
pins += self.bobs
for i in range(len(pins) - 1):
self._make_joints(pins[i + 1], pins[i], spacescene)
self.rods[i].add_updater(lambda mob, i=i: self._redraw_rods(mob, pins, i))
def end_swinging(self) -> None:
"""Stop swinging."""
spacescene = self.bobs[0].spacescene
spacescene.stop_rigidity(self.bobs)
class Pendulum(MultiPendulum):
def __init__(
self,
length=3.5,
initial_theta=0.3,
pivot_point=UP * 2,
rod_style={},
bob_style={
"radius": 0.25,
"color": ORANGE,
"fill_opacity": 1,
},
**kwargs,
):
"""A pendulum.
Parameters
----------
length
The length of the pendulum.
initial_theta
The initial angle of deviation.
rod_style
Parameters for ``Line``.
bob_style
Parameters for ``Circle``.
kwargs
Additional parameters for ``VGroup``.
Examples
--------
.. manim:: PendulumExample
:quality: low
from manim_physics import *
class PendulumExample(SpaceScene):
def construct(self):
pends = VGroup(*[Pendulum(i) for i in np.linspace(1, 5, 7)])
self.add(pends)
for p in pends:
self.make_rigid_body(*p.bobs)
p.start_swinging()
self.wait(10)
"""
self.length = length
self.pivot_point = pivot_point
point = self.pivot_point + (
RIGHT * np.sin(initial_theta) * length
+ DOWN * np.cos(initial_theta) * length
)
super().__init__(
point,
pivot_point=self.pivot_point,
rod_style=rod_style,
bob_style=bob_style,
**kwargs,
)
================================================
FILE: manim_physics/rigid_mechanics/rigid_mechanics.py
================================================
"""A gravity simulation space.
Most objects can be made into a rigid body (moves according to gravity
and collision) or a static body (stays still within the scene).
To use this feature, the :class:`~SpaceScene` must be used, to access
the specific functions of the space.
.. note::
* This feature utilizes the pymunk package. Although unnecessary,
it might make it easier if you knew a few things on how to use it.
`Official Documentation <http://www.pymunk.org/en/latest/pymunk.html>`_
`Youtube Tutorial <https://youtu.be/pRk---rdrbo>`_
* A low frame rate might cause some objects to pass static objects as
they don't register collisions finely enough. Trying to increase the
config frame rate might solve the problem.
Examples
--------
.. manim:: TwoObjectsFalling
from manim_physics import *
# use a SpaceScene to utilize all specific rigid-mechanics methods
class TwoObjectsFalling(SpaceScene):
def construct(self):
circle = Circle().shift(UP)
circle.set_fill(RED, 1)
circle.shift(DOWN + RIGHT)
rect = Square().shift(UP)
rect.rotate(PI / 4)
rect.set_fill(YELLOW_A, 1)
rect.shift(UP * 2)
rect.scale(0.5)
ground = Line([-4, -3.5, 0], [4, -3.5, 0])
wall1 = Line([-4, -3.5, 0], [-4, 3.5, 0])
wall2 = Line([4, -3.5, 0], [4, 3.5, 0])
walls = VGroup(ground, wall1, wall2)
self.add(walls)
self.play(
DrawBorderThenFill(circle),
DrawBorderThenFill(rect),
)
self.make_rigid_body(rect, circle) # Mobjects will move with gravity
self.make_static_body(walls) # Mobjects will stay in place
self.wait(5)
# during wait time, the circle and rect would move according to the simulate updater
"""
from __future__ import annotations
from typing import Tuple
from manim.constants import RIGHT, UP
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import Polygon, Polygram, Rectangle
from manim.mobject.mobject import Group, Mobject
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.scene.scene import Scene
from manim.utils.space_ops import angle_between_vectors
import numpy as np
import pymunk
__all__ = [
"Space",
"_step",
"_simulate",
"get_shape",
"get_angle",
"SpaceScene",
]
try:
# For manim < 0.15.0
from manim.mobject.opengl_compatibility import ConvertToOpenGL
except ModuleNotFoundError:
# For manim >= 0.15.0
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
class Space(Mobject, metaclass=ConvertToOpenGL):
def __init__(self, gravity: Tuple[float, float] = (0, -9.81), **kwargs):
"""An Abstract object for gravity.
Parameters
----------
gravity
The direction and strength of gravity.
"""
super().__init__(**kwargs)
self.space = pymunk.Space()
self.space.gravity = gravity
self.space.sleep_time_threshold = 5
class SpaceScene(Scene):
GRAVITY: Tuple[float, float] = 0, -9.81
def __init__(self, renderer=None, **kwargs):
"""A basis scene for all of rigid mechanics. The gravity vector
can be adjusted with ``self.GRAVITY``.
"""
self.space = Space(gravity=self.GRAVITY)
super().__init__(renderer=renderer, **kwargs)
def setup(self):
"""Used internally"""
self.add(self.space)
self.space.add_updater(_step)
def add_body(self, body: Mobject):
"""Bodies refer to pymunk's object.
This method ties Mobjects to their Bodies.
"""
if body.body != self.space.space.static_body:
self.space.space.add(body.body)
self.space.space.add(body.shape)
def make_rigid_body(
self,
*mobs: Mobject,
elasticity: float = 0.8,
density: float = 1,
friction: float = 0.8,
):
"""Make any mobject movable by gravity.
Equivalent to ``Scene``'s ``add`` function.
Parameters
----------
mobs
The mobs to be made rigid.
elasticity
density
friction
The attributes of the mobjects in regards to
interacting with other rigid and static objects.
"""
for mob in mobs:
if not hasattr(mob, "body"):
self.add(mob)
mob.body = pymunk.Body()
mob.body.position = mob.get_x(), mob.get_y()
get_angle(mob)
if not hasattr(mob, "angle"):
mob.angle = 0
mob.body.angle = mob.angle
get_shape(mob)
mob.shape.density = density
mob.shape.elasticity = elasticity
mob.shape.friction = friction
mob.spacescene = self
self.add_body(mob)
mob.add_updater(_simulate)
else:
if mob.body.is_sleeping:
mob.body.activate()
def make_static_body(
self, *mobs: Mobject, elasticity: float = 1, friction: float = 0.8
) -> None:
"""Make any mobject interactable by rigid objects.
Parameters
----------
mobs
The mobs to be made static.
elasticity
friction
The attributes of the mobjects in regards to
interacting with rigid objects.
"""
for mob in mobs:
if isinstance(mob, VGroup or Group):
return self.make_static_body(*mob)
mob.body = self.space.space.static_body
get_shape(mob)
mob.shape.elasticity = elasticity
mob.shape.friction = friction
self.add_body(mob)
def stop_rigidity(self, *mobs: Mobject) -> None:
"""Stop the mobjects rigidity"""
for mob in mobs:
if isinstance(mob, VGroup or Group):
self.stop_rigidity(*mob)
if hasattr(mob, "body"):
mob.body.sleep()
def _step(space, dt):
space.space.step(dt)
def _simulate(b):
x, y = b.body.position
b.move_to(x * RIGHT + y * UP)
b.rotate(b.body.angle - b.angle)
b.angle = b.body.angle
def get_shape(mob: VMobject) -> None:
"""Obtains the shape of the body from the mobject"""
if isinstance(mob, Circle):
mob.shape = pymunk.Circle(body=mob.body, radius=mob.radius)
elif isinstance(mob, Line):
mob.shape = pymunk.Segment(
mob.body,
(mob.get_start()[0], mob.get_start()[1]),
(mob.get_end()[0], mob.get_end()[1]),
mob.stroke_width - 3.95,
)
elif issubclass(type(mob), Rectangle):
width = np.linalg.norm(mob.get_vertices()[1] - mob.get_vertices()[0])
height = np.linalg.norm(mob.get_vertices()[2] - mob.get_vertices()[1])
mob.shape = pymunk.Poly.create_box(mob.body, (width, height))
elif issubclass(type(mob), Polygram):
vertices = [(a, b) for a, b, _ in mob.get_vertices() - mob.get_center()]
mob.shape = pymunk.Poly(mob.body, vertices)
else:
mob.shape = pymunk.Poly.create_box(mob.body, (mob.width, mob.height))
def get_angle(mob: VMobject) -> None:
"""Obtains the angle of the body from the mobject.
Used internally for updaters.
"""
if issubclass(type(mob), Polygon):
vec1 = mob.get_vertices()[0] - mob.get_vertices()[1]
vec2 = type(mob)().get_vertices()[0] - type(mob)().get_vertices()[1]
mob.angle = angle_between_vectors(vec1, vec2)
elif isinstance(mob, Line):
mob.angle = mob.get_angle()
================================================
FILE: manim_physics/wave.py
================================================
"""3D and 2D Waves module."""
from __future__ import annotations
from typing import Iterable, Optional
from manim import *
__all__ = [
"LinearWave",
"RadialWave",
"StandingWave",
]
try:
# For manim < 0.15.0
from manim.mobject.opengl_compatibility import ConvertToOpenGL
except ModuleNotFoundError:
# For manim >= 0.15.0
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
class RadialWave(Surface, metaclass=ConvertToOpenGL):
def __init__(
self,
*sources: Optional[np.ndarray],
wavelength: float = 1,
period: float = 1,
amplitude: float = 0.1,
x_range: Iterable[float] = [-5, 5],
y_range: Iterable[float] = [-5, 5],
**kwargs,
) -> None:
"""A 3D Surface with waves moving radially.
Parameters
----------
sources
The sources of disturbance.
wavelength
The wavelength of the wave.
period
The period of the wave.
amplitude
The amplitude of the wave.
x_range
The range of the wave in the x direction.
y_range
The range of the wave in the y direction.
kwargs
Additional parameters to be passed to :class:`~Surface`.
Examples
--------
.. manim:: RadialWaveExampleScene
class RadialWaveExampleScene(ThreeDScene):
def construct(self):
self.set_camera_orientation(60 * DEGREES, -45 * DEGREES)
wave = RadialWave(
LEFT * 2 + DOWN * 5, # Two source of waves
RIGHT * 2 + DOWN * 5,
checkerboard_colors=[BLUE_D],
stroke_width=0,
)
self.add(wave)
wave.start_wave()
self.wait()
wave.stop_wave()
"""
self.wavelength = wavelength
self.period = period
self.amplitude = amplitude
self.time = 0
self.kwargs = kwargs
self.sources = sources
super().__init__(
lambda u, v: np.array([u, v, self._wave_z(u, v, sources)]),
u_range=x_range,
v_range=y_range,
**kwargs,
)
def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -> float:
z = 0
for source in sources:
x0, y0, _ = source
z += self.amplitude * np.sin(
(2 * PI / self.wavelength) * ((u - x0) ** 2 + (v - y0) ** 2) ** 0.5
- 2 * PI * self.time / self.period
)
return z
def _update_wave(self, mob: Mobject, dt: float) -> None:
self.time += dt
mob.match_points(
Surface(
lambda u, v: np.array([u, v, self._wave_z(u, v, self.sources)]),
u_range=self.u_range,
v_range=self.v_range,
**self.kwargs,
)
)
def start_wave(self):
"""Animate the wave propagation."""
self.add_updater(self._update_wave)
def stop_wave(self):
"""Stop animating the wave propagation."""
self.remove_updater(self._update_wave)
class LinearWave(RadialWave):
def __init__(
self,
wavelength: float = 1,
period: float = 1,
amplitude: float = 0.1,
x_range: Iterable[float] = [-5, 5],
y_range: Iterable[float] = [-5, 5],
**kwargs,
) -> None:
"""A 3D Surface with waves in one direction.
Parameters
----------
wavelength
The wavelength of the wave.
period
The period of the wave.
amplitude
The amplitude of the wave.
x_range
The range of the wave in the x direction.
y_range
The range of the wave in the y direction.
kwargs
Additional parameters to be passed to :class:`~Surface`.
Examples
--------
.. manim:: LinearWaveExampleScene
class LinearWaveExampleScene(ThreeDScene):
def construct(self):
self.set_camera_orientation(60 * DEGREES, -45 * DEGREES)
wave = LinearWave()
self.add(wave)
wave.start_wave()
self.wait()
wave.stop_wave()
"""
super().__init__(
ORIGIN,
wavelength=wavelength,
period=period,
amplitude=amplitude,
x_range=x_range,
y_range=y_range,
**kwargs,
)
def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -> float:
return self.amplitude * np.sin(
(2 * PI / self.wavelength) * u - 2 * PI * self.time / self.period
)
class StandingWave(ParametricFunction):
def __init__(
self,
n: int = 2,
length: float = 4,
period: float = 1,
amplitude: float = 1,
**kwargs,
) -> None:
"""A 2D standing wave.
Parameters
----------
n
Harmonic number.
length
The length of the wave.
period
The time taken for one full oscillation.
amplitude
The maximum height of the wave.
kwargs
Additional parameters to be passed to :class:`~ParametricFunction`.
Examples
--------
.. manim:: StandingWaveExampleScene
from manim_physics import *
class StandingWaveExampleScene(Scene):
def construct(self):
wave1 = StandingWave(1)
wave2 = StandingWave(2)
wave3 = StandingWave(3)
wave4 = StandingWave(4)
waves = VGroup(wave1, wave2, wave3, wave4)
waves.arrange(DOWN).move_to(ORIGIN)
self.add(waves)
for wave in waves:
wave.start_wave()
self.wait()
"""
self.n = n
self.length = length
self.period = period
self.amplitude = amplitude
self.time = 0
self.kwargs = {**kwargs}
super().__init__(
lambda t: np.array([t, amplitude * np.sin(n * PI * t / length), 0]),
t_range=[0, length],
**kwargs,
)
self.shift([-self.length / 2, 0, 0])
def _update_wave(self, mob: Mobject, dt: float) -> None:
self.time += dt
mob.become(
ParametricFunction(
lambda t: np.array(
[
t,
self.amplitude
* np.sin(self.n * PI * t / self.length)
* np.cos(2 * PI * self.time / self.period),
0,
]
),
t_range=[0, self.length],
**self.kwargs,
).shift(self.wave_center + [-self.length / 2, 0, 0])
)
def start_wave(self):
self.wave_center = self.get_center()
self.add_updater(self._update_wave)
def stop_wave(self):
self.remove_updater(self._update_wave)
================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "manim-physics"
version = "0.4.0"
description = "Support physics simulation"
authors = ["Matheart <waautomationwong@gmail.com>"]
repository = "https://github.com/Matheart/manim-physics"
readme="README.md"
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
manim = "~0.18.0"
pymunk = "^6.6.0"
shapely = "^2.0.3"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = ">=23.11,<25.0"
pre-commit = "^3.5.0"
furo = "^2023.09.10"
Sphinx = "^7.2.6"
sphinx-copybutton = "^0.5.2"
sphinxcontrib-programoutput = "^0.17"
myst-parser = "^2.0.0"
matplotlib = "^3.8.2"
[build-system]
requires = ["setuptools", "poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins."manim.plugins"]
"manim_physics" = "manim_physics"
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/conftest.py
================================================
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from manim import config, tempconfig
def pytest_addoption(parser):
parser.addoption(
"--skip_slow",
action="store_true",
default=False,
help="Will skip all the slow marked tests. Slow tests are arbitrarily marked as such.",
)
parser.addoption(
"--show_diff",
action="store_true",
default=False,
help="Will show a visual comparison if a graphical unit test fails.",
)
parser.addoption(
"--set_test",
action="store_true",
default=False,
help="Will create the control data for EACH running tests. ",
)
def pytest_configure(config):
config.addinivalue_line("markers", "skip_end_to_end: mark test as end_to_end test")
def pytest_collection_modifyitems(config, items):
if not config.getoption("--skip_slow"):
return
else:
slow_skip = pytest.mark.skip(
reason="Slow test skipped due to --disable_slow flag.",
)
for item in items:
if "slow" in item.keywords:
item.add_marker(slow_skip)
@pytest.fixture(scope="session")
def python_version():
# use the same python executable as it is running currently
# rather than randomly calling using python or python3, which
# may create problems.
return sys.executable
@pytest.fixture
def reset_cfg_file():
cfgfilepath = Path(__file__).parent / "test_cli" / "manim.cfg"
original = cfgfilepath.read_text()
yield
cfgfilepath.write_text(original)
@pytest.fixture
def using_opengl_renderer():
"""Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir."""
with tempconfig({"renderer": "opengl"}):
yield
# as a special case needed to manually revert back to cairo
# due to side effects of setting the renderer
config.renderer = "cairo"
================================================
FILE: tests/test_electromagnetism.py
================================================
__module_test__ = "electromagnetism"
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
from manim_physics.electromagnetism.electrostatics import *
from manim_physics.electromagnetism.magnetostatics import *
@frames_comparison
def test_electric_field(scene):
charge1 = Charge(-1, LEFT + DOWN)
charge2 = Charge(2, RIGHT + DOWN)
charge3 = Charge(-1, UP)
field = ElectricField(charge1, charge2, charge3)
scene.add(charge1, charge2, charge3)
scene.add(field)
@frames_comparison
def test_magnetic_field(scene):
wire = Wire(Circle(2).rotate(PI / 2, UP))
field = MagneticField(wire)
scene.add(field, wire)
@frames_comparison(base_scene=ThreeDScene)
def test_magnetic_field_multiple_wires(scene):
wire1 = Wire(Circle(2).rotate(PI / 2, RIGHT).shift(UP * 2))
wire2 = Wire(Circle(2).rotate(PI / 2, RIGHT).shift(UP * -2))
mag_field = MagneticField(wire1, wire2)
scene.set_camera_orientation(PI / 3, PI / 4)
scene.add(wire1, wire2, mag_field)
================================================
FILE: tests/test_lensing.py
================================================
__module_test__ = "optics"
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
from manim_physics import *
@frames_comparison
def test_rays_lens(scene):
lens_style = {"fill_opacity": 0.5, "color": BLUE}
a = Lens(-100, 1, **lens_style).shift(LEFT)
a2 = Lens(100, 1, **lens_style).shift(RIGHT)
b = [
Ray(LEFT * 5 + UP * i, RIGHT, 8, [a, a2], color=RED)
for i in np.linspace(-2, 2, 10)
]
scene.add(a, a2, *b)
================================================
FILE: tests/test_pendulum.py
================================================
__module_test__ = "pendulum"
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
from manim_physics.rigid_mechanics.pendulum import *
@frames_comparison(base_scene=SpaceScene)
def test_pendulum(scene: SpaceScene):
pends = VGroup(*[Pendulum(i) for i in np.linspace(1, 5, 7)])
scene.add(pends)
for p in pends:
scene.make_rigid_body(*p.bobs)
p.start_swinging()
scene.wait()
@frames_comparison(base_scene=SpaceScene)
def test_multipendulum(scene):
p = MultiPendulum(RIGHT, LEFT)
scene.add(p)
scene.make_rigid_body(*p.bobs)
p.start_swinging()
scene.add(TracedPath(p.bobs[-1].get_center, stroke_color=BLUE))
scene.wait()
================================================
FILE: tests/test_rigid_mechanics.py
================================================
__module_test__ = "rigid_mechanics"
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
from manim_physics.rigid_mechanics.rigid_mechanics import *
@frames_comparison(base_scene=SpaceScene)
def test_rigid_mechanics(scene):
circle = Circle().shift(UP)
circle.set_fill(RED, 1)
circle.shift(DOWN + RIGHT)
rect = Square().shift(UP)
rect.rotate(PI / 4)
rect.set_fill(YELLOW_A, 1)
rect.shift(UP * 2)
rect.scale(0.5)
ground = Line([-4, -3.5, 0], [4, -3.5, 0])
wall1 = Line([-4, -3.5, 0], [-4, 3.5, 0])
wall2 = Line([4, -3.5, 0], [4, 3.5, 0])
walls = VGroup(ground, wall1, wall2)
scene.add(walls)
scene.play(
DrawBorderThenFill(circle),
DrawBorderThenFill(rect),
)
scene.make_rigid_body(rect, circle)
scene.make_static_body(walls)
scene.wait()
================================================
FILE: tests/test_wave.py
================================================
__module_test__ = "waves"
from manim import *
from manim.utils.testing.frames_comparison import frames_comparison
from manim_physics.wave import *
@frames_comparison()
def test_linearwave(scene):
wave = LinearWave()
wave.set(time=2)
scene.add(wave)
@frames_comparison()
def test_radialwave(scene):
wave = RadialWave(
LEFT * 2 + DOWN * 5, # Two source of waves
RIGHT * 2 + DOWN * 5,
checkerboard_colors=[BLUE_D],
stroke_width=0,
)
wave.set(time=2)
scene.add(wave)
@frames_comparison
def test_standingwave(scene):
wave1 = StandingWave(1)
wave2 = StandingWave(2)
wave3 = StandingWave(3)
wave4 = StandingWave(4)
waves = VGroup(wave1, wave2, wave3, wave4)
waves.arrange(DOWN).move_to(ORIGIN)
scene.add(waves)
for wave in waves:
wave.start_wave()
scene.wait()
gitextract_3nqz4oat/
├── .github/
│ ├── manimdependency.json
│ ├── scripts/
│ │ └── ci_build_cairo.py
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── README.md
├── docs/
│ ├── Makefile
│ ├── make.bat
│ ├── requirements.txt
│ ├── rtd-requirements.txt
│ └── source/
│ ├── _static/
│ │ ├── custom.css
│ │ └── responsiveSvg.js
│ ├── _templates/
│ │ └── autosummary/
│ │ ├── class.rst
│ │ └── module.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── contributing.rst
│ ├── index.rst
│ ├── reference.rst
│ └── reference_index/
│ ├── electromagnetism.rst
│ ├── optics.rst
│ ├── rigid_mechanics.rst
│ └── wave.rst
├── example.py
├── manim_physics/
│ ├── __init__.py
│ ├── electromagnetism/
│ │ ├── __init__.py
│ │ ├── electrostatics.py
│ │ └── magnetostatics.py
│ ├── optics/
│ │ ├── __init__.py
│ │ ├── lenses.py
│ │ └── rays.py
│ ├── rigid_mechanics/
│ │ ├── __init__.py
│ │ ├── pendulum.py
│ │ └── rigid_mechanics.py
│ └── wave.py
├── pyproject.toml
└── tests/
├── __init__.py
├── conftest.py
├── control_data/
│ ├── electromagnetism/
│ │ ├── electric_field.npz
│ │ ├── magnetic_field.npz
│ │ └── magnetic_field_multiple_wires.npz
│ ├── optics/
│ │ └── rays_lens.npz
│ ├── pendulum/
│ │ ├── multipendulum.npz
│ │ └── pendulum.npz
│ ├── rigid_mechanics/
│ │ └── rigid_mechanics.npz
│ └── waves/
│ ├── linearwave.npz
│ ├── radialwave.npz
│ └── standingwave.npz
├── test_electromagnetism.py
├── test_lensing.py
├── test_pendulum.py
├── test_rigid_mechanics.py
└── test_wave.py
SYMBOL INDEX (84 symbols across 16 files)
FILE: .github/scripts/ci_build_cairo.py
function is_ci (line 35) | def is_ci():
function download_file (line 39) | def download_file(url, path):
function verify_sha256sum (line 50) | def verify_sha256sum(path, sha256sum):
function extract_tar_xz (line 57) | def extract_tar_xz(path, directory):
function run_command (line 62) | def run_command(command, cwd=None, env=None):
function gha_group (line 70) | def gha_group(title: str) -> typing.Generator:
function set_env_var_gha (line 83) | def set_env_var_gha(name: str, value: str) -> None:
function get_ld_library_path (line 94) | def get_ld_library_path(prefix: Path) -> str:
function main (line 112) | def main():
FILE: docs/source/_static/responsiveSvg.js
function setColorScheme (line 13) | function setColorScheme(e) {
FILE: example.py
class MagneticFieldExample (line 4) | class MagneticFieldExample(ThreeDScene):
method construct (line 5) | def construct(self):
FILE: manim_physics/electromagnetism/electrostatics.py
class Charge (line 22) | class Charge(VGroup):
method __init__ (line 23) | def __init__(
class ElectricField (line 88) | class ElectricField(ArrowVectorField):
method __init__ (line 89) | def __init__(self, *charges: Charge, **kwargs) -> None:
method _field_func (line 123) | def _field_func(
FILE: manim_physics/electromagnetism/magnetostatics.py
class Wire (line 16) | class Wire(VMobject, metaclass=ConvertToOpenGL):
method __init__ (line 39) | def __init__(
class MagneticField (line 53) | class MagneticField(ArrowVectorField):
method __init__ (line 83) | def __init__(self, *wires: Wire, **kwargs):
method _field_func (line 98) | def _field_func(
FILE: manim_physics/optics/lenses.py
function intersection (line 27) | def intersection(vmob1: VMobject, vmob2: VMobject) -> Iterable[Iterable[...
function snell (line 40) | def snell(i_ang: float, n: float) -> float:
function antisnell (line 45) | def antisnell(r_ang: float, n: float) -> float:
class Lens (line 50) | class Lens(VMobject, metaclass=ConvertToOpenGL):
method __init__ (line 51) | def __init__(self, f: float, d: float, n: float = 1.52, **kwargs) -> N...
method C (line 101) | def C(self) -> Tuple[Iterable[float]]:
FILE: manim_physics/optics/rays.py
class Ray (line 18) | class Ray(Line):
method __init__ (line 19) | def __init__(
method propagate (line 65) | def propagate(self, *lenses: Lens) -> None:
method _sort_lens (line 123) | def _sort_lens(self, lenses: Iterable[Lens]) -> Iterable[Lens]:
method _sort_intersections (line 135) | def _sort_intersections(
FILE: manim_physics/rigid_mechanics/pendulum.py
class MultiPendulum (line 29) | class MultiPendulum(VGroup):
method __init__ (line 30) | def __init__(
method _make_joints (line 93) | def _make_joints(
method _redraw_rods (line 105) | def _redraw_rods(self, mob: Line, pins, i):
method start_swinging (line 116) | def start_swinging(self) -> None:
method end_swinging (line 126) | def end_swinging(self) -> None:
class Pendulum (line 132) | class Pendulum(MultiPendulum):
method __init__ (line 133) | def __init__(
FILE: manim_physics/rigid_mechanics/rigid_mechanics.py
class Space (line 87) | class Space(Mobject, metaclass=ConvertToOpenGL):
method __init__ (line 88) | def __init__(self, gravity: Tuple[float, float] = (0, -9.81), **kwargs):
class SpaceScene (line 102) | class SpaceScene(Scene):
method __init__ (line 105) | def __init__(self, renderer=None, **kwargs):
method setup (line 112) | def setup(self):
method add_body (line 117) | def add_body(self, body: Mobject):
method make_rigid_body (line 125) | def make_rigid_body(
method make_static_body (line 167) | def make_static_body(
method stop_rigidity (line 190) | def stop_rigidity(self, *mobs: Mobject) -> None:
function _step (line 199) | def _step(space, dt):
function _simulate (line 203) | def _simulate(b):
function get_shape (line 210) | def get_shape(mob: VMobject) -> None:
function get_angle (line 232) | def get_angle(mob: VMobject) -> None:
FILE: manim_physics/wave.py
class RadialWave (line 23) | class RadialWave(Surface, metaclass=ConvertToOpenGL):
method __init__ (line 24) | def __init__(
method _wave_z (line 85) | def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -...
method _update_wave (line 95) | def _update_wave(self, mob: Mobject, dt: float) -> None:
method start_wave (line 106) | def start_wave(self):
method stop_wave (line 110) | def stop_wave(self):
class LinearWave (line 115) | class LinearWave(RadialWave):
method __init__ (line 116) | def __init__(
method _wave_z (line 165) | def _wave_z(self, u: float, v: float, sources: Iterable[np.ndarray]) -...
class StandingWave (line 171) | class StandingWave(ParametricFunction):
method __init__ (line 172) | def __init__(
method _update_wave (line 228) | def _update_wave(self, mob: Mobject, dt: float) -> None:
method start_wave (line 246) | def start_wave(self):
method stop_wave (line 250) | def stop_wave(self):
FILE: tests/conftest.py
function pytest_addoption (line 11) | def pytest_addoption(parser):
function pytest_configure (line 32) | def pytest_configure(config):
function pytest_collection_modifyitems (line 36) | def pytest_collection_modifyitems(config, items):
function python_version (line 49) | def python_version():
function reset_cfg_file (line 57) | def reset_cfg_file():
function using_opengl_renderer (line 65) | def using_opengl_renderer():
FILE: tests/test_electromagnetism.py
function test_electric_field (line 11) | def test_electric_field(scene):
function test_magnetic_field (line 21) | def test_magnetic_field(scene):
function test_magnetic_field_multiple_wires (line 28) | def test_magnetic_field_multiple_wires(scene):
FILE: tests/test_lensing.py
function test_rays_lens (line 9) | def test_rays_lens(scene):
FILE: tests/test_pendulum.py
function test_pendulum (line 10) | def test_pendulum(scene: SpaceScene):
function test_multipendulum (line 20) | def test_multipendulum(scene):
FILE: tests/test_rigid_mechanics.py
function test_rigid_mechanics (line 10) | def test_rigid_mechanics(scene):
FILE: tests/test_wave.py
function test_linearwave (line 10) | def test_linearwave(scene):
function test_radialwave (line 17) | def test_radialwave(scene):
function test_standingwave (line 29) | def test_standingwave(scene):
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
{
"path": ".github/manimdependency.json",
"chars": 1133,
"preview": "{\n \"windows\": {\n \"tinytex\": [\n \"standalone\",\n \"preview\",\n \"doublestroke\",\n "
},
{
"path": ".github/scripts/ci_build_cairo.py",
"chars": 6843,
"preview": "# Logic is as follows:\n# 1. Download cairo source code: https://cairographics.org/releases/cairo-<version>.tar.xz\n# 2. V"
},
{
"path": ".github/workflows/ci.yml",
"chars": 6312,
"preview": "name: CI\n\nconcurrency:\n group: ${{ github.ref }}\n cancel-in-progress: true\n\non:\n push:\n branches:\n - main\n p"
},
{
"path": ".gitignore",
"chars": 1781,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".pre-commit-config.yaml",
"chars": 521,
"preview": "default_stages: [commit, push]\nfail_fast: false\nrepos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n rev:"
},
{
"path": ".readthedocs.yml",
"chars": 300,
"preview": "version: 2\r\nbuild:\r\n os: ubuntu-22.04\r\n\r\n tools:\r\n python: \"3.10\"\r\n\r\n apt_packages:\r\n - libpango1.0-dev\r\n - "
},
{
"path": "README.md",
"chars": 711,
"preview": "# manim-physics \n## Introduction\nThis is a 2D physics simulation plugin that allows you to generate complicated\nscenes i"
},
{
"path": "docs/Makefile",
"chars": 1108,
"preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
},
{
"path": "docs/make.bat",
"chars": 857,
"preview": "@ECHO OFF\r\n\r\npushd %~dp0\\source\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXB"
},
{
"path": "docs/requirements.txt",
"chars": 66,
"preview": "furo\nmyst-parser\nsphinx<5.1\nsphinx-copybutton\nsphinxext-opengraph\n"
},
{
"path": "docs/rtd-requirements.txt",
"chars": 39,
"preview": "jupyterlab\nsphinxcontrib-programoutput\n"
},
{
"path": "docs/source/_static/custom.css",
"chars": 1572,
"preview": "@media (prefers-color-scheme: dark) {\r\n span.nc {\r\n text-decoration: none !important;\r\n }\r\n}\r\n\r\n.admonition"
},
{
"path": "docs/source/_static/responsiveSvg.js",
"chars": 1925,
"preview": "window.addEventListener(\"load\", function () {\r\n const styleElements = []\r\n const colorSchemeQuery = window.matchMe"
},
{
"path": "docs/source/_templates/autosummary/class.rst",
"chars": 691,
"preview": "{{ name | escape | underline}}\n\nQualified name: ``{{ fullname | escape }}``\n\n.. currentmodule:: {{ module }}\n\n.. autocla"
},
{
"path": "docs/source/_templates/autosummary/module.rst",
"chars": 1124,
"preview": "{{ name | escape | underline }}\n\n.. currentmodule:: {{ fullname }}\n\n.. automodule:: {{ fullname }}\n\n {% block attribut"
},
{
"path": "docs/source/changelog.rst",
"chars": 2415,
"preview": "=========\r\nChangelog\r\n=========\r\n\r\n**v0.4.1**\r\n==========\r\nBugfix\r\n------\r\n- `#34 <https://github.com/Matheart/manim-phy"
},
{
"path": "docs/source/conf.py",
"chars": 4282,
"preview": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common op"
},
{
"path": "docs/source/contributing.rst",
"chars": 488,
"preview": "Contributing\r\n============\r\n\r\nContributions are welcome! The repository owner (Matheart) might not be\r\navailable, any pu"
},
{
"path": "docs/source/index.rst",
"chars": 660,
"preview": "Manim Physics\r\n-------------\r\n\r\nWelcome to the `manim-physics` official documentation!\r\n\r\nInstallation\r\n++++++++++++\r\n``"
},
{
"path": "docs/source/reference.rst",
"chars": 184,
"preview": "Reference\r\n---------\r\n\r\n\r\n.. toctree::\r\n :maxdepth: 2\r\n\r\n reference_index/electromagnetism\r\n reference_index/optic"
},
{
"path": "docs/source/reference_index/electromagnetism.rst",
"chars": 195,
"preview": "Electromagnetism\r\n================\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n :toctree: ../reference\r\n\r"
},
{
"path": "docs/source/reference_index/optics.rst",
"chars": 147,
"preview": "Optics\r\n------\r\n\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n :toctree: ../reference\r\n\r\n ~optics.lense"
},
{
"path": "docs/source/reference_index/rigid_mechanics.rst",
"chars": 186,
"preview": "Rigid Mechanics\r\n---------------\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n :toctree: ../reference\r\n\r\n "
},
{
"path": "docs/source/reference_index/wave.rst",
"chars": 114,
"preview": "Waves\r\n========\r\n\r\n.. currentmodule:: manim_physics\r\n\r\n.. autosummary::\r\n :toctree: ../reference\r\n\r\n ~wave\r\n\r\n"
},
{
"path": "example.py",
"chars": 281,
"preview": "from manim_physics import *\r\n\r\n\r\nclass MagneticFieldExample(ThreeDScene):\r\n def construct(self):\r\n wire = Wire"
},
{
"path": "manim_physics/__init__.py",
"chars": 301,
"preview": "__version__ = \"0.2.3\"\n\nfrom manim import *\n\nfrom .electromagnetism.electrostatics import *\nfrom .electromagnetism.magnet"
},
{
"path": "manim_physics/electromagnetism/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "manim_physics/electromagnetism/electrostatics.py",
"chars": 4496,
"preview": "\"\"\"Electrostatics module\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nfrom manim import normalize"
},
{
"path": "manim_physics/electromagnetism/magnetostatics.py",
"chars": 3093,
"preview": "\"\"\"Magnetostatics module\"\"\"\r\n\r\nfrom __future__ import annotations\r\nimport itertools as it\r\nfrom typing import Iterable, "
},
{
"path": "manim_physics/optics/__init__.py",
"chars": 108,
"preview": "\"\"\"A lensing module.\r\n\r\nCurrently only shows refraction in lenses and not\r\ntotal internal reflection.\r\n\"\"\"\r\n"
},
{
"path": "manim_physics/optics/lenses.py",
"chars": 3466,
"preview": "\"\"\"Lenses for refracting Rays.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Iterable, Tuple\n\nfrom manim imp"
},
{
"path": "manim_physics/optics/rays.py",
"chars": 5026,
"preview": "\"\"\"Rays of light. Refracted by Lenses.\"\"\"\r\n\r\nfrom __future__ import annotations\r\nfrom typing import Iterable\r\n\r\nfrom man"
},
{
"path": "manim_physics/rigid_mechanics/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "manim_physics/rigid_mechanics/pendulum.py",
"chars": 5267,
"preview": "r\"\"\"Pendulums.\n\n:class:`~MultiPendulum` and :class:`~Pendulum` both stem from the\n:py:mod:`~rigid_mechanics` feature.\n\n\""
},
{
"path": "manim_physics/rigid_mechanics/rigid_mechanics.py",
"chars": 7977,
"preview": "\"\"\"A gravity simulation space.\n\nMost objects can be made into a rigid body (moves according to gravity\nand collision) or"
},
{
"path": "manim_physics/wave.py",
"chars": 7381,
"preview": "\"\"\"3D and 2D Waves module.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Iterable, Optional\n\nfrom manim impo"
},
{
"path": "pyproject.toml",
"chars": 805,
"preview": "[tool.poetry]\nname = \"manim-physics\"\nversion = \"0.4.0\"\ndescription = \"Support physics simulation\"\nauthors = [\"Matheart <"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/conftest.py",
"chars": 2041,
"preview": "from __future__ import annotations\r\n\r\nimport sys\r\nfrom pathlib import Path\r\n\r\nimport pytest\r\n\r\nfrom manim import config,"
},
{
"path": "tests/test_electromagnetism.py",
"chars": 1031,
"preview": "__module_test__ = \"electromagnetism\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_compa"
},
{
"path": "tests/test_lensing.py",
"chars": 485,
"preview": "__module_test__ = \"optics\"\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom"
},
{
"path": "tests/test_pendulum.py",
"chars": 717,
"preview": "__module_test__ = \"pendulum\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nf"
},
{
"path": "tests/test_rigid_mechanics.py",
"chars": 867,
"preview": "__module_test__ = \"rigid_mechanics\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_compar"
},
{
"path": "tests/test_wave.py",
"chars": 868,
"preview": "__module_test__ = \"waves\"\n\nfrom manim import *\nfrom manim.utils.testing.frames_comparison import frames_comparison\n\nfrom"
}
]
// ... and 10 more files (download for full content)
About this extraction
This page contains the full source code of the Matheart/manim-physics GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (76.0 KB), approximately 20.7k tokens, and a symbol index with 84 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.