pyphysica 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyphysica-0.1.0/LICENSE +21 -0
- pyphysica-0.1.0/MANIFEST.in +4 -0
- pyphysica-0.1.0/PKG-INFO +161 -0
- pyphysica-0.1.0/README.md +130 -0
- pyphysica-0.1.0/examples/example_physics.py +218 -0
- pyphysica-0.1.0/pyproject.toml +48 -0
- pyphysica-0.1.0/setup.cfg +4 -0
- pyphysica-0.1.0/src/physica/__init__.py +1319 -0
- pyphysica-0.1.0/src/pyphysica.egg-info/PKG-INFO +161 -0
- pyphysica-0.1.0/src/pyphysica.egg-info/SOURCES.txt +12 -0
- pyphysica-0.1.0/src/pyphysica.egg-info/dependency_links.txt +1 -0
- pyphysica-0.1.0/src/pyphysica.egg-info/requires.txt +6 -0
- pyphysica-0.1.0/src/pyphysica.egg-info/top_level.txt +1 -0
- pyphysica-0.1.0/tests/test_physica.py +360 -0
pyphysica-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rohan Hariharan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyphysica-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyphysica
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A symbolic + numeric physics DSL: decorator-based forces, time-varying vectors, and sympy-derived equations of motion.
|
|
5
|
+
Author: Rohan Hariharan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rohanhariharan/pyphysica
|
|
8
|
+
Project-URL: Repository, https://github.com/rohanhariharan/pyphysica
|
|
9
|
+
Project-URL: Issues, https://github.com/rohanhariharan/pyphysica/issues
|
|
10
|
+
Keywords: physics,simulation,sympy,symbolic,cas,dsl,mechanics
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy
|
|
26
|
+
Requires-Dist: sympy
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# pyphysica
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/pyphysica/)
|
|
35
|
+
[](https://pypi.org/project/pyphysica/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
|
|
38
|
+
A symbolic + numeric physics DSL for Python. Declare forces as class attributes,
|
|
39
|
+
let `pyphysica` integrate them numerically — or derive the equations of motion
|
|
40
|
+
symbolically with [SymPy](https://www.sympy.org/).
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from physica import Object2, Vector2, continuous, update
|
|
44
|
+
|
|
45
|
+
ball = Object2("ball", mass=0.45, position=Vector2(0, 10), velocity=Vector2(5, 0))
|
|
46
|
+
|
|
47
|
+
@continuous(ball)
|
|
48
|
+
class Gravity:
|
|
49
|
+
gravity = Vector2(0, -9.81)
|
|
50
|
+
|
|
51
|
+
@update(ball)
|
|
52
|
+
def physics(obj, dt):
|
|
53
|
+
obj.integrate(dt)
|
|
54
|
+
|
|
55
|
+
for _ in range(100):
|
|
56
|
+
physics(ball, 0.016)
|
|
57
|
+
|
|
58
|
+
print(ball.position) # <8.0, -2.5568>
|
|
59
|
+
print(ball.kinetic_energy())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pip install pyphysica
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
From source:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
git clone https://github.com/rohanhariharan/pyphysica
|
|
72
|
+
cd pyphysica
|
|
73
|
+
pip install -e ".[dev]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What it does
|
|
77
|
+
|
|
78
|
+
**Decorator-based forces.** Annotate a class with `@continuous(obj)` to register
|
|
79
|
+
each `Vector2` attribute as a per-step force, or `@impulse(obj)` for a one-shot
|
|
80
|
+
impulse (`Δv = J/m`).
|
|
81
|
+
|
|
82
|
+
**Time-varying forces.** `TimeVector` lets a force change over time, either
|
|
83
|
+
symbolically in `T` or as a Python callable:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from physica import TimeVector, T
|
|
87
|
+
|
|
88
|
+
@continuous(obj)
|
|
89
|
+
class Engine:
|
|
90
|
+
drive = TimeVector.of(lambda t: Vector2(4 * sp.sin(t), 0)) # callable
|
|
91
|
+
thrust = TimeVector(0, 120.0 - 3 * T) # symbolic in T
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Symbolic equations of motion.** Since forces may be expressions in `T`,
|
|
95
|
+
`Object2` can return them as SymPy:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
ax, ay = obj.equation_of_motion() # expressions in T
|
|
99
|
+
ax, ay = obj.acceleration_func() # T-bound Funcs
|
|
100
|
+
vy = obj.velocity_func() # integrate once from rest
|
|
101
|
+
y = obj.position_func() # integrate twice from rest
|
|
102
|
+
sol = obj.closed_form() # dsolve, zero initial conditions
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Multi-body systems:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from physica import combine, describe_system
|
|
109
|
+
|
|
110
|
+
print(describe_system([rocket, booster]))
|
|
111
|
+
system = combine([rocket, booster]) # total mass, summed net force, per-body accel
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Symbolic math (`Func`/`Lim`).** A thin, ergonomic wrapper over SymPy:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from physica import Func, Lim, x, T
|
|
118
|
+
import sympy as sp
|
|
119
|
+
|
|
120
|
+
f = Func(x**2 - 1)
|
|
121
|
+
f(3) # 8
|
|
122
|
+
f[1] # derivative -> 2*x
|
|
123
|
+
f[-1] # antiderivative
|
|
124
|
+
f[[0, 1]] # definite integral
|
|
125
|
+
(f @ Func(x + 1)) # composition
|
|
126
|
+
Lim(sp.sin(x)/x, x, 0) # 1
|
|
127
|
+
|
|
128
|
+
g = Func(3 * T + 1, var=T) # variable-aware: defaults to x
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Vectors and more.** `Vector2`, `Vector3`, `Quaternion`, and `Matrix` round out
|
|
132
|
+
the math layer.
|
|
133
|
+
|
|
134
|
+
## Example
|
|
135
|
+
|
|
136
|
+
See [`examples/example_physics.py`](examples/example_physics.py) for seven
|
|
137
|
+
worked examples — projectile with wind and an impulsive kick, a time-varying
|
|
138
|
+
drive, multi-object systems, `simulate()` history, `Func` bootstrap over derived
|
|
139
|
+
equations, and both the exact and small-angle pendulum.
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
python examples/example_physics.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
pytest
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Physics notes
|
|
153
|
+
|
|
154
|
+
- Integration is semi-implicit Euler, which introduces `O(dt)` energy drift.
|
|
155
|
+
Halving the timestep halves the drift.
|
|
156
|
+
- `small-angle` checks against the analytic SHM solution agree to `< 1e-3`.
|
|
157
|
+
- Equations of motion are derived from Newton's second law: `a = F_net / m`.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# pyphysica
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pyphysica/)
|
|
4
|
+
[](https://pypi.org/project/pyphysica/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A symbolic + numeric physics DSL for Python. Declare forces as class attributes,
|
|
8
|
+
let `pyphysica` integrate them numerically — or derive the equations of motion
|
|
9
|
+
symbolically with [SymPy](https://www.sympy.org/).
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from physica import Object2, Vector2, continuous, update
|
|
13
|
+
|
|
14
|
+
ball = Object2("ball", mass=0.45, position=Vector2(0, 10), velocity=Vector2(5, 0))
|
|
15
|
+
|
|
16
|
+
@continuous(ball)
|
|
17
|
+
class Gravity:
|
|
18
|
+
gravity = Vector2(0, -9.81)
|
|
19
|
+
|
|
20
|
+
@update(ball)
|
|
21
|
+
def physics(obj, dt):
|
|
22
|
+
obj.integrate(dt)
|
|
23
|
+
|
|
24
|
+
for _ in range(100):
|
|
25
|
+
physics(ball, 0.016)
|
|
26
|
+
|
|
27
|
+
print(ball.position) # <8.0, -2.5568>
|
|
28
|
+
print(ball.kinetic_energy())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install pyphysica
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
From source:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
git clone https://github.com/rohanhariharan/pyphysica
|
|
41
|
+
cd pyphysica
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
**Decorator-based forces.** Annotate a class with `@continuous(obj)` to register
|
|
48
|
+
each `Vector2` attribute as a per-step force, or `@impulse(obj)` for a one-shot
|
|
49
|
+
impulse (`Δv = J/m`).
|
|
50
|
+
|
|
51
|
+
**Time-varying forces.** `TimeVector` lets a force change over time, either
|
|
52
|
+
symbolically in `T` or as a Python callable:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from physica import TimeVector, T
|
|
56
|
+
|
|
57
|
+
@continuous(obj)
|
|
58
|
+
class Engine:
|
|
59
|
+
drive = TimeVector.of(lambda t: Vector2(4 * sp.sin(t), 0)) # callable
|
|
60
|
+
thrust = TimeVector(0, 120.0 - 3 * T) # symbolic in T
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Symbolic equations of motion.** Since forces may be expressions in `T`,
|
|
64
|
+
`Object2` can return them as SymPy:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
ax, ay = obj.equation_of_motion() # expressions in T
|
|
68
|
+
ax, ay = obj.acceleration_func() # T-bound Funcs
|
|
69
|
+
vy = obj.velocity_func() # integrate once from rest
|
|
70
|
+
y = obj.position_func() # integrate twice from rest
|
|
71
|
+
sol = obj.closed_form() # dsolve, zero initial conditions
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Multi-body systems:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from physica import combine, describe_system
|
|
78
|
+
|
|
79
|
+
print(describe_system([rocket, booster]))
|
|
80
|
+
system = combine([rocket, booster]) # total mass, summed net force, per-body accel
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Symbolic math (`Func`/`Lim`).** A thin, ergonomic wrapper over SymPy:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from physica import Func, Lim, x, T
|
|
87
|
+
import sympy as sp
|
|
88
|
+
|
|
89
|
+
f = Func(x**2 - 1)
|
|
90
|
+
f(3) # 8
|
|
91
|
+
f[1] # derivative -> 2*x
|
|
92
|
+
f[-1] # antiderivative
|
|
93
|
+
f[[0, 1]] # definite integral
|
|
94
|
+
(f @ Func(x + 1)) # composition
|
|
95
|
+
Lim(sp.sin(x)/x, x, 0) # 1
|
|
96
|
+
|
|
97
|
+
g = Func(3 * T + 1, var=T) # variable-aware: defaults to x
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Vectors and more.** `Vector2`, `Vector3`, `Quaternion`, and `Matrix` round out
|
|
101
|
+
the math layer.
|
|
102
|
+
|
|
103
|
+
## Example
|
|
104
|
+
|
|
105
|
+
See [`examples/example_physics.py`](examples/example_physics.py) for seven
|
|
106
|
+
worked examples — projectile with wind and an impulsive kick, a time-varying
|
|
107
|
+
drive, multi-object systems, `simulate()` history, `Func` bootstrap over derived
|
|
108
|
+
equations, and both the exact and small-angle pendulum.
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
python examples/example_physics.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Tests
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pip install -e ".[dev]"
|
|
118
|
+
pytest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Physics notes
|
|
122
|
+
|
|
123
|
+
- Integration is semi-implicit Euler, which introduces `O(dt)` energy drift.
|
|
124
|
+
Halving the timestep halves the drift.
|
|
125
|
+
- `small-angle` checks against the analytic SHM solution agree to `< 1e-3`.
|
|
126
|
+
- Equations of motion are derived from Newton's second law: `a = F_net / m`.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Example uses of the physica DSL.
|
|
2
|
+
|
|
3
|
+
Run with: python3 example_physics.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sympy as sp
|
|
7
|
+
from physica import Object2, Vector2, TimeVector, Time, T, x, Func, Lim, continuous, impulse, update, simulate, combine, describe_system
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# 1. Projectile with gravity, a steady wind, and an impulsive kick
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
print("=" * 60)
|
|
14
|
+
print("1. Projectile: gravity + wind + kick")
|
|
15
|
+
print("=" * 60)
|
|
16
|
+
|
|
17
|
+
ball = Object2("ball", mass=0.45, position=Vector2(0, 10), velocity=Vector2(5, 0))
|
|
18
|
+
|
|
19
|
+
@continuous(ball)
|
|
20
|
+
class Gravity:
|
|
21
|
+
gravity = Vector2(0, -9.81)
|
|
22
|
+
|
|
23
|
+
@continuous(ball)
|
|
24
|
+
class Wind:
|
|
25
|
+
wind = Vector2(2, 0)
|
|
26
|
+
|
|
27
|
+
@impulse(ball)
|
|
28
|
+
class Kick:
|
|
29
|
+
force = Vector2(50, 20)
|
|
30
|
+
|
|
31
|
+
@update(ball)
|
|
32
|
+
def physics(obj, dt):
|
|
33
|
+
obj.integrate(dt)
|
|
34
|
+
obj.clear_impulses()
|
|
35
|
+
|
|
36
|
+
for _ in range(100):
|
|
37
|
+
physics(ball, 0.016)
|
|
38
|
+
|
|
39
|
+
print(f"final position: {ball.position}")
|
|
40
|
+
print(f"speed: {ball.speed()}, KE: {ball.kinetic_energy()}")
|
|
41
|
+
print()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# 2. Time-varying force (sinusoidal drive) + closed form via sympy
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
print("=" * 60)
|
|
48
|
+
print("2. Time-varying drive + symbolic combination")
|
|
49
|
+
print("=" * 60)
|
|
50
|
+
|
|
51
|
+
car = Object2("car", mass=2.0)
|
|
52
|
+
|
|
53
|
+
@continuous(car)
|
|
54
|
+
class Road:
|
|
55
|
+
push = Vector2(1.5, 0)
|
|
56
|
+
|
|
57
|
+
@continuous(car)
|
|
58
|
+
class Engine:
|
|
59
|
+
drive = TimeVector.of(lambda t: Vector2(4 * sp.sin(t), 0))
|
|
60
|
+
|
|
61
|
+
print(car.describe())
|
|
62
|
+
print("\nclosed form (dsolve):")
|
|
63
|
+
for solution in car.closed_form():
|
|
64
|
+
print(" ", solution)
|
|
65
|
+
print()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# 3. Multiple objects combined into one system
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
print("=" * 60)
|
|
72
|
+
print("3. Multi-object system")
|
|
73
|
+
print("=" * 60)
|
|
74
|
+
|
|
75
|
+
rocket = Object2("rocket", mass=50.0)
|
|
76
|
+
booster = Object2("booster", mass=10.0)
|
|
77
|
+
|
|
78
|
+
@continuous(rocket)
|
|
79
|
+
class Thrust:
|
|
80
|
+
force = TimeVector(0, 120.0 - 3 * T) # thrust falls off with time
|
|
81
|
+
|
|
82
|
+
@continuous(booster)
|
|
83
|
+
class Boost:
|
|
84
|
+
force = Vector2(0, 30)
|
|
85
|
+
|
|
86
|
+
print(describe_system([rocket, booster]))
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# 4. Using a shared clock + simulate(), then replay the history
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
print("=" * 60)
|
|
94
|
+
print("4. simulate() with a shared clock and history")
|
|
95
|
+
print("=" * 60)
|
|
96
|
+
|
|
97
|
+
clock = Time()
|
|
98
|
+
probe = Object2("probe", mass=1.0, position=Vector2(0, 5))
|
|
99
|
+
|
|
100
|
+
@continuous(probe)
|
|
101
|
+
class Spring:
|
|
102
|
+
restoring = TimeVector.of(lambda t: Vector2(0, -2 * probe.position.y))
|
|
103
|
+
|
|
104
|
+
history = simulate(probe, dt=0.1, length=10, time=clock)
|
|
105
|
+
states = list(history.values())
|
|
106
|
+
print(f"clock.t = {clock.t}")
|
|
107
|
+
print(f"steps recorded = {len(history)}")
|
|
108
|
+
print("first state:", states[0][0]["position"])
|
|
109
|
+
print("last state:", states[-1][0]["position"])
|
|
110
|
+
print()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# 5. Bootstrap syntax (Func/Lim) applied to the derived equations of motion
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
print("=" * 60)
|
|
117
|
+
print("5. Func bootstrap over the physics formulae")
|
|
118
|
+
print("=" * 60)
|
|
119
|
+
|
|
120
|
+
block = Object2("block", mass=2.0)
|
|
121
|
+
|
|
122
|
+
@continuous(block)
|
|
123
|
+
class Gravity:
|
|
124
|
+
gravity = Vector2(0, -9.81)
|
|
125
|
+
|
|
126
|
+
@continuous(block)
|
|
127
|
+
class Ramp:
|
|
128
|
+
push = TimeVector.of(lambda t: Vector2(2 * t, 0))
|
|
129
|
+
|
|
130
|
+
ax, ay = block.acceleration_func()
|
|
131
|
+
print(f"ax(T) = {ax} ay(T) = {ay}")
|
|
132
|
+
|
|
133
|
+
# Pretty syntax on the T-bound Funcs
|
|
134
|
+
print(f"vx(T) = ax[-1] = {ax[-1]}")
|
|
135
|
+
print(f"x(T) = ax[-2] = {ax[-2]}")
|
|
136
|
+
print(f"jerk = ax[1] = {ax[1]}")
|
|
137
|
+
print(f"ax solves zero at T = {ax.solve()}")
|
|
138
|
+
print(f"average ax over [0, 3] = {ax[[0, 3]] / 3}")
|
|
139
|
+
|
|
140
|
+
# The classic Func(x) behaviour is untouched
|
|
141
|
+
f = Func(x**2 - 1)
|
|
142
|
+
print(f"Func(x**2-1)(3) = {f(3)}, f[1] = {f[1]}, f[[0,1]] = {f[[0,1]]}")
|
|
143
|
+
print(f"Lim(sin(x)/x, x, 0) = {Lim(sp.sin(x)/x, x, 0)}")
|
|
144
|
+
print()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# 6. Pendulum — using the angular DOF (angle / angular_velocity / torque)
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
print("=" * 60)
|
|
151
|
+
print("6. Pendulum")
|
|
152
|
+
print("=" * 60)
|
|
153
|
+
|
|
154
|
+
import math
|
|
155
|
+
|
|
156
|
+
L, g, m = 1.0, 9.81, 0.5 # rod length, gravity, bob mass
|
|
157
|
+
|
|
158
|
+
bob = Object2("bob", mass=m)
|
|
159
|
+
bob.moment = m * L ** 2 # I = mL^2 for a point mass on a massless rod
|
|
160
|
+
bob.angle = math.pi / 2 # start horizontal
|
|
161
|
+
bob.angular_velocity = 0.0
|
|
162
|
+
|
|
163
|
+
@update(bob)
|
|
164
|
+
def pendulum(obj, dt):
|
|
165
|
+
obj.torque = -m * g * L * math.sin(obj.angle) # restoring torque
|
|
166
|
+
obj.integrate(dt)
|
|
167
|
+
obj.position = Vector2(L * math.sin(obj.angle), -L * math.cos(obj.angle))
|
|
168
|
+
|
|
169
|
+
def energy(o):
|
|
170
|
+
return 0.5 * o.moment * o.angular_velocity ** 2 - m * g * L * math.cos(o.angle)
|
|
171
|
+
|
|
172
|
+
E0 = float(energy(bob))
|
|
173
|
+
print(f"E(0) = {E0:.6g}")
|
|
174
|
+
for _ in range(200):
|
|
175
|
+
pendulum(bob, 0.01)
|
|
176
|
+
|
|
177
|
+
print(f"angle = {float(bob.angle):.6f} rad")
|
|
178
|
+
print(f"omega = {float(bob.angular_velocity):.6f} rad/s")
|
|
179
|
+
# Semi-implicit Euler drifts in energy, first-order in dt: halve dt -> halve drift.
|
|
180
|
+
print(f"E(t) = {float(energy(bob)):.6g} (E0 = {E0:.6g}; drift is O(dt))")
|
|
181
|
+
print(f"bob position (x, y) = {bob.position}")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# 7. Pendulum, small-angle approximation sin(theta) ~ theta
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
print("=" * 60)
|
|
189
|
+
print("7. Pendulum (small-angle: sin(theta) ~ theta)")
|
|
190
|
+
print("=" * 60)
|
|
191
|
+
|
|
192
|
+
theta0 = 0.1
|
|
193
|
+
small = Object2("small", mass=m)
|
|
194
|
+
small.moment = m * L ** 2
|
|
195
|
+
small.angle = theta0
|
|
196
|
+
small.angular_velocity = 0.0
|
|
197
|
+
|
|
198
|
+
@update(small)
|
|
199
|
+
def small_pendulum(obj, dt):
|
|
200
|
+
obj.torque = -m * g * L * obj.angle # linear restoring torque
|
|
201
|
+
obj.integrate(dt)
|
|
202
|
+
obj.position = Vector2(L * math.sin(obj.angle), -L * math.cos(obj.angle))
|
|
203
|
+
|
|
204
|
+
omega0 = math.sqrt(g / L)
|
|
205
|
+
dt = 0.0005
|
|
206
|
+
max_err = 0.0
|
|
207
|
+
for i in range(1, 4001):
|
|
208
|
+
small_pendulum(small, dt)
|
|
209
|
+
t = i * dt
|
|
210
|
+
analytic = theta0 * math.cos(omega0 * t) # exact SHM solution
|
|
211
|
+
max_err = max(max_err, abs(float(small.angle) - analytic))
|
|
212
|
+
|
|
213
|
+
print(f"omega = sqrt(g/L) = {omega0:.6f} rad/s")
|
|
214
|
+
print(f"period = 2*pi/omega = {2*math.pi/omega0:.6f} s")
|
|
215
|
+
print(f"amplitude-independent (SHM), unlike the exact sin(theta) pendulum")
|
|
216
|
+
print(f"max |numeric - {theta0}*cos(omega*t)| over 2 s = {max_err:.2e}")
|
|
217
|
+
|
|
218
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyphysica"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A symbolic + numeric physics DSL: decorator-based forces, time-varying vectors, and sympy-derived equations of motion."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["physics", "simulation", "sympy", "symbolic", "cas", "dsl", "mechanics"]
|
|
13
|
+
authors = [{ name = "Rohan Hariharan" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Intended Audience :: Education",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy",
|
|
29
|
+
"sympy",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/rohanhariharan/pyphysica"
|
|
34
|
+
Repository = "https://github.com/rohanhariharan/pyphysica"
|
|
35
|
+
Issues = "https://github.com/rohanhariharan/pyphysica/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=7", "build"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
package-dir = { "" = "src" }
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
pythonpath = ["src"]
|