Last active
June 2, 2024 07:24
-
-
Save thquinn/39c140d572dbc8d67eed702f3a8af836 to your computer and use it in GitHub Desktop.
A not-very-fast symbolic physics simulation using SymPy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import sage.all as sage | |
from itertools import combinations | |
from PIL import Image, ImageDraw | |
from typing import cast, Dict | |
from enum import Enum | |
os.system("clear") | |
class SimCircle: | |
center: sage.vector | |
radius: sage.Expression | |
velocity: sage.vector | |
def __init__(self, center: sage.vector, radius: sage.Expression, velocity: sage.vector = sage.vector([0, 0])): | |
self.center = center | |
self.radius = radius | |
self.velocity = velocity | |
self.first = False | |
def to_tuple(self): | |
return sage.vector([self.center[0], self.center[1], self.velocity[0], self.velocity[1]]) | |
def __str__(self): | |
return f'SimCircle center: {self.center}, radius: {self.radius}, velocity: {self.velocity}' | |
class SimSegment: | |
p1: sage.vector | |
p2: sage.vector | |
def __init__(self, p1: sage.vector, p2: sage.vector): | |
self.p1 = p1 | |
self.p2 = p2 | |
def length(self): | |
return sage.sqrt((self.p2[0] - self.p1[0]) ** 2 + (self.p2[1] - self.p1[1]) ** 2) | |
def contains_box(self, p): | |
xs = sorted([self.p1[0], self.p2[0]]) | |
ys = sorted([self.p1[1], self.p2[1]]) | |
return bool(p[0] >= xs[0]) and bool(p[0] <= xs[1]) and bool(p[1] >= ys[0]) and bool(p[1] <= ys[1]) | |
def projection(self, p): | |
AB = self.p2 - self.p1 | |
AC = p - self.p1 | |
AD = AB * (AB.dot_product(AC)) / AB.dot_product(AB) | |
return self.p1 + AD | |
def __str__(self): | |
return f'SimSegment p1: {self.p1}, p2: {self.p2}' | |
class SimState(Enum): | |
INCOMPLETE = 1 | |
COMPLETE_WITH_PERIOD = 2 | |
INVALID_MULTIPLE_COLLISIONS = 3 | |
INVALID_SPAWN_OVERLAP = 4 | |
INVALID_TIMEOUT = 5 | |
class Sim: | |
SPAWN_RATE: sage.Expression = sage.Integer(1) | |
TIMEOUT: sage.Expression = SPAWN_RATE * sage.Integer(50) | |
SPAWN_HEIGHT: sage.Expression = sage.Integer(3) | |
DESPAWN_HEIGHT: sage.Expression = sage.Integer(-10) | |
CIRCLE_RADIUS: sage.Expression = sage.Rational('1/3') | |
GRAVITY: sage.Expression = sage.Integer(-3) | |
SEGMENT_LENGTH: sage.Expression = sage.Rational('3/2') | |
SEGMENT_SEPARATION: sage.Expression = sage.Integer(3) | |
COEFFICIENT_OF_RESTITUTION: sage.Expression = sage.Rational('1/1') | |
FRAME_TIME = sage.Rational('1/10') | |
t: sage.Expression | |
circles: list[SimCircle] | |
segments: list[SimSegment] | |
next_spawn_t: sage.Expression | |
previous_configurations: Dict[list, sage.Expression] | |
state: SimState | |
def __init__(self, theta1:sage.Expression, theta2:sage.Expression): | |
self.t = sage.Integer(0) | |
self.circles = [] | |
left_segment_center = sage.vector([0, 0]) | |
right_segment_center = sage.vector([Sim.SEGMENT_SEPARATION, 0]) | |
self.segments = [ | |
SimSegment(left_segment_center - sage.vector([sage.cos(theta1), -sage.sin(theta1)]) * Sim.SEGMENT_LENGTH / 2, left_segment_center + sage.vector([sage.cos(theta1), -sage.sin(theta1)]) * Sim.SEGMENT_LENGTH / 2), | |
SimSegment(right_segment_center - sage.vector([sage.cos(theta2), sage.sin(theta2)]) * Sim.SEGMENT_LENGTH / 2, right_segment_center + sage.vector([sage.cos(theta2), sage.sin(theta2)]) * Sim.SEGMENT_LENGTH / 2) | |
] | |
self.next_spawn_t = sage.Integer(0) | |
self.previous_configurations = dict() | |
self.state = SimState.INCOMPLETE | |
def iterate(self): | |
collisions = [] | |
for circle_pair in combinations(self.circles, 2): | |
next_collision = self.t + circle_collision_next_time(*circle_pair) | |
collisions.append([next_collision, *circle_pair]) | |
for segment in self.segments: | |
for circle in self.circles: | |
next_collision = self.t + circle_segment_collision_next_time(circle, segment, Sim.GRAVITY) | |
collisions.append([next_collision, circle, segment]) | |
min_time_collisions = [] | |
min_time: sage.Expression = sage.infinity | |
for collision in collisions: | |
if collision[0] < min_time: | |
min_time_collisions = [] | |
min_time = collision[0] | |
if collision[0] <= min_time: | |
min_time_collisions.append(collision) | |
min_time = min_time.full_simplify() if min_time != sage.oo else sage.oo | |
print(f'min collision time: {min_time.n()}, next spawn: {self.next_spawn_t.n()}') | |
if min_time >= self.next_spawn_t: | |
self.advance_t_to(self.next_spawn_t) | |
self.next_spawn_t += Sim.SPAWN_RATE | |
# TODO: check for circles in spawn area | |
self.circles.append(SimCircle(sage.vector([0, Sim.SPAWN_HEIGHT]), Sim.CIRCLE_RADIUS)) | |
if (len(self.circles) == 1): | |
self.circles[0].first = True | |
return -1 | |
# TODO: check for circles involved in multiple collisions | |
self.advance_t_to(min_time) | |
for collision in min_time_collisions: | |
c1 = cast(SimCircle, collision[1]) | |
if isinstance(collision[2], SimSegment): | |
seg = cast(SimSegment, collision[2]) | |
perform_circle_point_collision(c1, seg.projection(c1.center), Sim.COEFFICIENT_OF_RESTITUTION) | |
else: | |
c2 = cast(SimCircle, collision[2]) | |
perform_circle_collision(c1, c2, Sim.COEFFICIENT_OF_RESTITUTION) | |
self.circles = [c for c in self.circles if c.center[1] > Sim.DESPAWN_HEIGHT] | |
print('circles in the sim:', len(self.circles)) | |
configuration = [c.to_tuple() for c in self.circles] | |
configuration.sort() | |
configuration = str(configuration) | |
if configuration in self.previous_configurations: | |
prev_config_time = self.previous_configurations[configuration] | |
config_delta = self.t - prev_config_time | |
self.state = SimState.COMPLETE_WITH_PERIOD | |
return config_delta / Sim.SPAWN_RATE | |
self.previous_configurations[configuration] = self.t | |
if self.t > Sim.TIMEOUT: | |
self.state = SimState.INVALID_TIMEOUT | |
return -1 | |
def advance_t_to(self, next_t: sage.Expression): | |
if (self.t == 0): | |
self.save_img(); | |
print(f'advancing to t={next_t.n()}') | |
next_frame = sage.ceil(self.t / Sim.FRAME_TIME) * Sim.FRAME_TIME | |
while (next_frame < next_t): | |
self.advance_t_to_impl(next_frame) | |
next_frame += Sim.FRAME_TIME | |
self.save_img(); | |
self.advance_t_to_impl(next_t) | |
def advance_t_to_impl(self, next_t: sage.Expression): | |
delta: sage.Expression = next_t - self.t | |
if delta == 0: | |
return | |
for circle in self.circles: | |
circle.center = sage.vector([ | |
circle.center[0] + circle.velocity[0] * delta, | |
circle.center[1] + circle.velocity[1] * delta + Sim.GRAVITY * delta ** 2 / 2 | |
]) | |
circle.velocity = sage.vector([ | |
circle.velocity[0], | |
circle.velocity[1] + delta * Sim.GRAVITY | |
]) | |
self.t = next_t | |
def save_img(self): | |
pixels_per_unit = 100 | |
bounds_min = (-3, -3) | |
bounds_max = (6, 5) | |
def coor_convert(p: sage.vector): | |
px = (p[0].n() - bounds_min[0]) * pixels_per_unit | |
py = (bounds_max[1] - p[1].n()) * pixels_per_unit | |
return (px, py) | |
img = Image.new("RGBA", ((bounds_max[0] - bounds_min[0]) * pixels_per_unit, (bounds_max[1] - bounds_min[1]) * pixels_per_unit)) | |
draw = ImageDraw.Draw(img) | |
draw.rectangle([(0,0), img.size], fill = (0, 0, 0) ) | |
for segment in self.segments: | |
draw.line([*coor_convert(segment.p1), *coor_convert(segment.p2)], width=5, fill=(255, 255, 255)) | |
for circle in self.circles: | |
draw.ellipse( | |
[ | |
*coor_convert(circle.center - sage.vector([circle.radius, -circle.radius])), | |
*coor_convert(circle.center + sage.vector([circle.radius, -circle.radius])) | |
], | |
fill=(255, 255, 255) | |
) | |
img.save(f'img{self.t.n()}.png', 'PNG') | |
def circle_collision_next_time(c1: SimCircle, c2: SimCircle): | |
sage.forget() | |
print('circle collision next time in') | |
t = sage.var('t') | |
sage.assume(t > 0) | |
sage.assume(t, 'real') | |
eq = sage.sqrt( (c1.velocity[0] * t + c1.center[0] - c2.velocity[0] * t - c2.center[0]) ** 2 + (c1.velocity[1] * t + c1.center[1] - c2.velocity[1] * t - c2.center[1]) ** 2) == c1.radius + c2.radius | |
collision_times = eq.solve(t, to_poly_solve=True) | |
print('circle collision next time out') | |
return min([c.rhs() for c in collision_times]) if len(collision_times) > 0 else sage.oo | |
def circle_segment_collision_next_time(c: SimCircle, s: SimSegment, g: sage.Expression): | |
sage.forget() | |
if c.first: | |
print('circle-segment collision next time in') | |
print(c, s, g) | |
t = sage.var('t') | |
# sage.assume(t > 0) | |
# sage.assume(t, 'real') | |
p = sage.vector([c.center[0] + c.velocity[0] * t, c.center[1] + c.velocity[1] * t + g * t ** 2 / 2]) | |
# Find collisions with the line defined by the segment. | |
eq = sage.abs_symbolic((s.p2[0] - s.p1[0]) * (p[1] - s.p1[1]) - (p[0] - s.p1[0]) * (s.p2[1] - s.p1[1])) / s.length() == c.radius | |
if c.first: | |
print('line equation', eq) | |
collision_times = eq.solve(t, to_poly_solve=True) | |
##### | |
# ^ TODO -- BUG! ^ | |
##### | |
# to_poly_solve is missing solutions. e.g. the following: | |
# t = var('t') | |
# f = 2/3*abs(-1/245760000*sqrt(2)*((4*(5*sqrt(-32*sqrt(2) + 288) - 72)^2 - sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) + 3600*sqrt(2) + 2880*sqrt(-32*sqrt(2) + 288) - 49536)*(sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) - 3600*sqrt(2))*t*sqrt(-32*sqrt(2) + 288)/(abs(-1/2400*(5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 1/9600*sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) - 3/8*sqrt(2) - 3/10*sqrt(-32*sqrt(2) + 288) + 129/25)^2 + abs(-1/9600*sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) + 3/8*sqrt(2))^2) + 69120000*sqrt(2) - 552960000) + 1/245760000*sqrt(2)*(((4*(5*sqrt(-32*sqrt(2) + 288) - 72)^2 - sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) + 3600*sqrt(2) + 2880*sqrt(-32*sqrt(2) + 288) - 49536)^2*sqrt(-32*sqrt(2) + 288)/(abs(-1/2400*(5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 1/9600*sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) - 3/8*sqrt(2) - 3/10*sqrt(-32*sqrt(2) + 288) + 129/25)^2 + abs(-1/9600*sqrt(2)*(sqrt(2)*((5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 900*sqrt(2) + 720*sqrt(-32*sqrt(2) + 288) - 12384) + 1800) + 3/8*sqrt(2))^2) - 46080000*sqrt(-32*sqrt(2) + 288))*t - 276480000*t^2 - 76800*(5*sqrt(-32*sqrt(2) + 288) - 72)^2 + 69120000*sqrt(2) - 55296000*sqrt(-32*sqrt(2) + 288) + 951091200)) == (1/3) | |
# solve(f, t, to_poly_solve=True) | |
# test on https://sagecell.sagemath.org/ | |
# with to_poly_solve, returns [] — without, returns a simplified equation: | |
# 1/16*abs(18*sqrt(2)*t^2 + 3*sqrt(2)*t*sqrt(-32*sqrt(2) + 288) - 36*sqrt(2) - 8) == (1/2) | |
# that Wolfram can find real solutions to. adding assumptions doesn't help | |
##### | |
# maybe even worse, if you simplify the equation and square it on both sides you get: | |
# t = var('t') | |
# f = 1/256*(18*sqrt(2)*t^2 + 3*sqrt(2)*t*sqrt(-32*sqrt(2) + 288) - 36*sqrt(2) - 8)**2 == (1/4) | |
# s = solve(f, t, to_poly_solve=True) | |
# [s.rhs().n() for s in s] | |
# which returns four complex solutions. but if you plug the same equation into Wolfram, you get back the same 4 real solutions as the non-squared version, the smallest positive one is the one we want. | |
# does Sage sometimes return WRONG solutions, and exclude solutions even when to_poly_solve is False? | |
##### | |
# Filter out collisions where the collision point is not on the segment. | |
collision_times = [t_val for t_val in collision_times if s.contains_box(s.projection(p.subs(t = t_val.rhs())))] | |
if c.first: | |
print('collision times', collision_times) | |
# Add collisions with the endpoints themselves. This may include times where the circle contains part/all of the segment. | |
# We don't really care about that, though: as long as we always process the earliest event, we shouldn't be letting | |
# circles pass through segments to begin with. | |
for endpoint in [s.p1, s.p2]: | |
# TODO: can we square both sides of this kind of equation? | |
eq = sage.sqrt((endpoint[0] - p[0]) ** 2 + (endpoint[1] - p[1]) ** 2) == c.radius | |
if c.first: | |
print('endpoint equation', eq) | |
endpoint_solutions = eq.solve(t, to_poly_solve=True) | |
collision_times.extend(endpoint_solutions) | |
if c.first: | |
print('collision times w/ endpoints', collision_times) | |
collision_times = [c for c in collision_times if c.rhs() in sage.RR and c.rhs() > sage.Integer(0)] | |
if c.first: | |
print('collision times after filter', collision_times) | |
print('circle-segment collision next time out') | |
return min([c.rhs() for c in collision_times]) if len(collision_times) > 0 else sage.oo | |
def perform_circle_collision(c1: SimCircle, c2: SimCircle, e: sage.Expression): | |
dv = c2.velocity - c1.velocity | |
normal = (c2.center - c1.center).normalized() | |
vn = dv[0] * normal[0] + dv[1] * normal[1] | |
j = (1 + e) * vn / (1 / c1.radius + 1 / c2.radius) | |
c1.velocity += j * normal / c1.radius | |
c2.velocity -= j * normal / c2.radius | |
def perform_circle_point_collision(c: SimCircle, p: sage.vector, e: sage.Expression): | |
normal = (c.center - p).normalized() | |
vn = c.velocity[0] * normal[0] + c.velocity[1] * normal[1] | |
c.velocity -= normal * vn * (1 + e) | |
# c1 = SimCircle(sage.vector([0, 0]), 1, sage.vector([0, 0])) | |
# c2 = SimCircle(sage.vector([5, 0]), 1, sage.vector([-1, 0])) | |
# print(circle_collision_next_time(c1, c2)) | |
# s = SimSegment(sage.vector([-2, -5]), sage.vector([20, -6])) | |
# print(circle_segment_collision_next_time(c1, s, -1)) | |
sim = Sim(sage.pi / 4, sage.pi / 4) | |
while sim.state == SimState.INCOMPLETE: | |
print(f'iterating: {sim.t}') | |
period = sim.iterate() | |
if (sim.state == SimState.COMPLETE_WITH_PERIOD): | |
print(f'we actually found a period: {period}') | |
print(sim.state) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# rewrite in SymEngine? https://github.com/symengine/symengine | |
# seems to not have an analogue to solve() | |
# or: https://www.sagemath.org/ | |
# we might be able to specify a range for t in sp.solve to reduce solve times? | |
# algo: | |
# - start at t=0 with two line segments and an empty list of circles, and repeat the following | |
# - find collision times between all pairs of circles and all circle/segment pairs | |
# - filter out all collisions that don't take place at the min time | |
# - if the min time is less than or equal to the next spawn time: | |
# - if any one circle is involved in multiple collisions, the result is INVALID: MULTIPLE COLLISIONS | |
# - process all collisions | |
# - advance time to the min collision time | |
# - if the min time is greater than the next spawn time: | |
# - advance time to the next spawn time | |
# - remove all circles with y < some negative number | |
# - if we are at a spawn time, spawn a new circle | |
# - if the new circle overlaps an existing circle, the result is INVALID: SPAWN OVERLAP | |
# - check if we've seen this configuration of circles before | |
# - if we have, return (elapsed time since we last saw this configuration) / (spawn period) | |
# - otherwise, record the time and configuration | |
# - if the time is over some maximum, the result is INVALID: TIMEOUT | |
import sympy as sp | |
from itertools import combinations | |
from PIL import Image, ImageDraw | |
from typing import cast, Dict | |
from enum import Enum | |
import os | |
os.system("clear") | |
class SimCircle: | |
circle: sp.Circle | |
velocity: sp.Point | |
def __init__(self, circle: sp.Circle): | |
self.circle = circle | |
self.velocity = sp.Point2D(0, 0) | |
def to_tuple(self): | |
return [self.circle.center.x, self.circle.center.y, self.velocity.x, self.velocity.y] | |
def __str__(self) -> str: | |
return f'SimCircle {self.circle}, {self.velocity}' | |
class SimState(Enum): | |
INCOMPLETE = 1 | |
COMPLETE_WITH_PERIOD = 2 | |
INVALID_MULTIPLE_COLLISIONS = 3 | |
INVALID_SPAWN_OVERLAP = 4 | |
INVALID_TIMEOUT = 5 | |
class Sim: | |
SPAWN_RATE: sp.Number = 1 | |
TIMEOUT: sp.Number = SPAWN_RATE * 50 | |
SPAWN_HEIGHT: sp.Number = 3 | |
DESPAWN_HEIGHT: sp.Number = -10 | |
CIRCLE_RADIUS: sp.Number = sp.Rational(1, 3) | |
GRAVITY: sp.Number = -3 | |
SEGMENT_LENGTH: sp.Number = sp.Rational(3, 2) | |
SEGMENT_SEPARATION: sp.Number = 3 | |
COEFFICIENT_OF_RESTITUTION: sp.Number = sp.Rational(9, 10) | |
FRAME_TIME = sp.Rational(1, 10) | |
t: sp.Number | |
circles: list[SimCircle] | |
segments: list[sp.Segment2D] | |
next_spawn_t: sp.Number | |
previous_configurations: Dict[list, sp.Number] | |
state: SimState | |
def __init__(self, theta1:sp.Number, theta2:sp.Number): | |
self.t = sp.S.Zero | |
self.circles = [] | |
left_segment_center = sp.Point2D(0, 0) | |
right_segment_center = sp.Point2D(Sim.SEGMENT_SEPARATION, 0) | |
self.segments = [ | |
sp.Segment2D(left_segment_center - sp.Point2D(sp.cos(theta1), -sp.sin(theta1)) * Sim.SEGMENT_LENGTH / 2, left_segment_center + sp.Point2D(sp.cos(theta1), -sp.sin(theta1)) * Sim.SEGMENT_LENGTH / 2), | |
sp.Segment2D(right_segment_center - sp.Point2D(sp.cos(theta2), sp.sin(theta2)) * Sim.SEGMENT_LENGTH / 2, right_segment_center + sp.Point2D(sp.cos(theta2), sp.sin(theta2)) * Sim.SEGMENT_LENGTH / 2) | |
] | |
self.next_spawn_t = sp.S(0) | |
self.previous_configurations = dict() | |
self.state = SimState.INCOMPLETE | |
def iterate(self): | |
collisions = [] | |
for circle_pair in combinations(self.circles, 2): | |
next_collision = self.t + sp.Min(*circle_collision_times(*circle_pair)) | |
collisions.append([next_collision, *circle_pair]) | |
for segment in self.segments: | |
for circle in self.circles: | |
next_collision = self.t + sp.Min(*circle_line_segment_collision_times(circle, segment, Sim.GRAVITY)) | |
collisions.append([next_collision, circle, segment]) | |
min_time_collisions = [] | |
min_time: sp.Number = sp.oo | |
for collision in collisions: | |
if collision[0] < min_time: | |
min_time_collisions = [] | |
min_time = collision[0] | |
if collision[0] <= min_time: | |
min_time_collisions.append(collision) | |
print(f'min collision time: {min_time.evalf()}, next spawn: {self.next_spawn_t.evalf()}') | |
if min_time >= self.next_spawn_t: | |
self.advance_t_to(self.next_spawn_t) | |
self.next_spawn_t += Sim.SPAWN_RATE | |
# TODO: check for circles in spawn area | |
self.circles.append(SimCircle(sp.Circle(sp.Point2D(0, Sim.SPAWN_HEIGHT), Sim.CIRCLE_RADIUS))) | |
return -1 | |
# TODO: check for circles involved in multiple collisions | |
self.advance_t_to(min_time) | |
for collision in min_time_collisions: | |
c1 = cast(SimCircle, collision[1]) | |
if isinstance(collision[2], sp.Segment2D): | |
seg = cast(sp.Segment2D, collision[2]) | |
perform_circle_point_collision(c1, seg.projection(c1.circle.center), Sim.COEFFICIENT_OF_RESTITUTION) | |
else: | |
c2 = cast(SimCircle, collision[2]) | |
perform_circle_collision(c1, c2, Sim.COEFFICIENT_OF_RESTITUTION) | |
self.circles = [c for c in self.circles if c.circle.center.y > Sim.DESPAWN_HEIGHT] | |
configuration = [c.to_tuple() for c in self.circles] | |
configuration.sort() | |
configuration = str(configuration) | |
if configuration in self.previous_configurations: | |
prev_config_time = self.previous_configurations[configuration] | |
config_delta = prev_config_time - self.t | |
self.state = SimState.COMPLETE_WITH_PERIOD | |
return config_delta / Sim.SPAWN_RATE | |
self.previous_configurations[configuration] = self.t | |
if self.t > Sim.TIMEOUT: | |
self.state = SimState.INVALID_TIMEOUT | |
return -1 | |
def advance_t_to(self, next_t: sp.Number): | |
if (self.t == 0): | |
self.save_img(); | |
print(f'advancing to t={next_t.evalf()}') | |
next_frame = sp.ceiling(self.t / Sim.FRAME_TIME) * Sim.FRAME_TIME | |
while (next_frame < next_t): | |
self.advance_t_to_impl(next_frame) | |
next_frame += Sim.FRAME_TIME | |
self.save_img(); | |
self.advance_t_to_impl(next_t) | |
def advance_t_to_impl(self, next_t: sp.Number): | |
next_t = sp.simplify(next_t) | |
delta: sp.Number = next_t - self.t | |
if delta.is_zero: | |
return | |
for circle in self.circles: | |
circle.circle = sp.Circle( | |
sp.Point2D(sp.simplify(circle.circle.center.x + circle.velocity.x * delta), | |
sp.simplify(circle.circle.center.y + circle.velocity.y * delta + Sim.GRAVITY * delta ** 2 / 2)), | |
circle.circle.radius | |
) | |
circle.velocity = sp.Point2D( | |
circle.velocity.x, | |
sp.simplify(circle.velocity.y + delta * Sim.GRAVITY), | |
) | |
self.t = next_t | |
def save_img(self): | |
pixels_per_unit = 100 | |
bounds_min = (-3, -3) | |
bounds_max = (6, 5) | |
def coor_convert(p: sp.Point2D): | |
px = (p.x.evalf() - bounds_min[0]) * pixels_per_unit | |
py = (bounds_max[1] - p.y.evalf()) * pixels_per_unit | |
return (px, py) | |
img = Image.new("RGBA", ((bounds_max[0] - bounds_min[0]) * pixels_per_unit, (bounds_max[1] - bounds_min[1]) * pixels_per_unit)) | |
draw = ImageDraw.Draw(img) | |
draw.rectangle([(0,0), img.size], fill = (0, 0, 0) ) | |
for segment in self.segments: | |
draw.line([*coor_convert(segment.p1), *coor_convert(segment.p2)], width=5, fill=(255, 255, 255)) | |
for circle in self.circles: | |
draw.ellipse([*coor_convert(circle.circle.center - sp.Point2D(circle.circle.radius, -circle.circle.radius)), | |
*coor_convert(circle.circle.center + sp.Point2D(circle.circle.radius, -circle.circle.radius))], | |
fill=(255, 255, 255)) | |
img.save(f'img{self.t.evalf()}.png', 'PNG') | |
def circle_collision_times(c1: SimCircle, c2: SimCircle): | |
t = sp.symbols('t', real=True, positive=True) | |
eq = sp.Eq(sp.sqrt( (c1.velocity.x * t + c1.circle.center.x - c2.velocity.x * t - c2.circle.center.x) ** 2 + (c1.velocity.y * t + c1.circle.center.y - c2.velocity.y * t - c2.circle.center.y) ** 2), c1.circle.radius + c2.circle.radius) | |
collision_times = sp.solve(eq, t) | |
collision_times = [time for time in collision_times if time.is_real and time > 0] | |
return collision_times | |
def circle_line_segment_collision_times(c: SimCircle, s: sp.Segment2D, g: sp.Number): | |
print('=== circle_line_segment_collision_times ===') | |
print(c) | |
print(s) | |
t = sp.symbols('t', real=True, positive=True) | |
p = sp.Point2D(c.circle.center.x + c.velocity.x * t, c.circle.center.y + c.velocity.y * t + g * t ** 2 / 2) | |
# Find collisions with the line defined by the segment. | |
line = sp.Line2D(s) | |
eq = sp.Eq(line.distance(p), c.circle.radius) | |
print('colliding with line') | |
collision_times = list(sp.solve(eq, t)) | |
# Filter out collisions where the collision point is not on the segment. | |
collision_times = [t_val for t_val in collision_times if s.contains(line.projection(p.subs(t, t_val)))] | |
# Add collisions with the endpoints themselves. This may include times where the circle contains part/all of the segment. | |
# We don't really care about that, though: as long as we always process the earliest event, we shouldn't be letting | |
# circles pass through segments to begin with. | |
for endpoint in [s.p1, s.p2]: | |
eq = sp.Eq(sp.simplify(endpoint.distance(p)), c.circle.radius) | |
print(f'colliding with endpoint: {eq}') | |
endpoint_solutions = sp.solve(eq, t, manual=True) | |
print(f'got back: {endpoint_solutions}') | |
collision_times.extend(endpoint_solutions) | |
collision_times = [time for time in collision_times if time.is_real and time > 0] | |
print('=== done ===') | |
return collision_times | |
def perform_circle_collision(c1: SimCircle, c2: SimCircle, e: sp.Number): | |
dv = c2.velocity - c1.velocity | |
normal = (c2.circle.center - c1.circle.center).unit | |
vn = dv.x * normal.x + dv.y * normal.y | |
j = (1 + e) * vn / (1 / c1.circle.radius + 1 / c2.circle.radius) | |
c1.velocity += j * normal / c1.circle.radius | |
c2.velocity -= j * normal / c2.circle.radius | |
def perform_circle_point_collision(c: SimCircle, p: sp.Point2D, e: sp.Number): | |
normal = (c.circle.center - p).unit | |
vn = c.velocity.x * normal.x + c.velocity.y * normal.y | |
c.velocity -= normal * vn * (1 + e) | |
sim = Sim(sp.pi / 4, sp.pi / 4) | |
while sim.state == SimState.INCOMPLETE: | |
print(f'iterating: {sim.t}') | |
period = sim.iterate() | |
if (period > 0): | |
print(f'we actually found a period: {period}') | |
print(sim.state) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment