Skip to content

Commit d7fd801

Browse files
committed
Day 20
1 parent 2df9c1d commit d7fd801

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

20_particle_swarm.rb

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
particles = ARGF.map { |l|
2+
l.scan(/-?\d+/).map(&method(:Integer)).each_slice(3).map(&:freeze).freeze
3+
}.freeze
4+
5+
# Pick an arbitrary large time and hope it gives the right result?
6+
T = 10000
7+
# Simply comparing magnitudes is fraught with peril:
8+
# p 0 0 0 v 1 0 0 a 1 0 0
9+
# p 0 0 0 v -2 0 0 a 1 0 0
10+
puts particles.each_with_index.min_by { |(p, v, a), _|
11+
# Note the T * (T + 1), rather than just T * T
12+
# because this is discrete, not continuous.
13+
p.zip(v, a).sum { |p0, v0, a0| (p0 + v0 * T + a0 * T * (T + 1) / 2).abs }
14+
}.last
15+
16+
GIVE_UP_AFTER = 20
17+
cycles_since_last_collision = 0
18+
last_size = particles.size
19+
20+
# TODO: should I detect when we're overflowing compression?
21+
half_coord = 1 << 19
22+
compress = ->((x, y, z)) {
23+
(x + half_coord) << 40 | (y + half_coord) << 20 | z + half_coord
24+
}
25+
particles = particles.map { |part| part.map(&compress) }.freeze
26+
27+
puts loop {
28+
particles.each { |part|
29+
part[1] += part[2]
30+
part[0] += part[1]
31+
}
32+
particles = particles.group_by(&:first).select { |_, v|
33+
v.size == 1
34+
}.values.flatten(1)
35+
cycles_since_last_collision = 0 if particles.size != last_size
36+
break particles.size if (cycles_since_last_collision += 1) > GIVE_UP_AFTER
37+
last_size = particles.size
38+
}
+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
require 'benchmark'
2+
3+
bench_candidates = []
4+
5+
bench_candidates << def fixed_time(particles)
6+
give_up_after = 20
7+
cycles_since_last_collision = 0
8+
last_size = particles.size
9+
loop {
10+
particles.each { |p, v, a|
11+
3.times { |i|
12+
v[i] += a[i]
13+
p[i] += v[i]
14+
}
15+
}
16+
particles = particles.group_by(&:first).select { |_, v|
17+
v.size == 1
18+
}.values.flatten(1)
19+
cycles_since_last_collision = 0 if particles.size != last_size
20+
return particles.size if (cycles_since_last_collision += 1) > give_up_after
21+
last_size = particles.size
22+
}
23+
end
24+
25+
bench_candidates << def compress(particles)
26+
give_up_after = 20
27+
cycles_since_last_collision = 0
28+
last_size = particles.size
29+
half_coord = 1 << 19
30+
compress = ->((x, y, z)) {
31+
(x + half_coord) << 40 | (y + half_coord) << 20 | z + half_coord
32+
}
33+
particles.map! { |pva| pva.map(&compress) }
34+
35+
loop {
36+
particles.each { |part|
37+
part[1] += part[2]
38+
part[0] += part[1]
39+
}
40+
particles = particles.group_by(&:first).select { |_, v|
41+
v.size == 1
42+
}.values.flatten(1)
43+
cycles_since_last_collision = 0 if particles.size != last_size
44+
return particles.size if (cycles_since_last_collision += 1) > give_up_after
45+
last_size = particles.size
46+
}
47+
end
48+
49+
slow_bench_candidates = []
50+
# too slow (1000 particles means 499500 pairs)
51+
slow_bench_candidates << def detect_time(particles)
52+
# Max possible collision time.
53+
# Calculating this collision time just takes too long.
54+
max_t = 0
55+
particles.combination(2) { |(p1, v1, a1), (p2, v2, a2)|
56+
times = [0, 1, 2].map { |i|
57+
# When will they collide in the ith dimension?
58+
# Because we change velocity before position:
59+
# 0.5a1t(t + 1) + v1t + p1 = 0.5a2t(t + 1) + v2t + p2
60+
# 0.5a1t^2 + v1t + 0.5a2t + p1 = 0.5a2t^2 + v2t + 0.5a2t + p2
61+
a = (a1[i] - a2[i]) / 2.0
62+
b = a + v1[i] - v2[i]
63+
c = p1[i] - p2[i]
64+
65+
if a == 0 && b == 0
66+
# Do not collide.
67+
next [] if c != 0
68+
# May collide in this dimension at any time.
69+
nil
70+
elsif a == 0
71+
# Equal acceleration, only need to deal w/ vel and pos
72+
# v1t + p1 = v2t + p2
73+
[-c / b.to_f].reject(&:negative?)
74+
else
75+
d = b * b - 4 * a * c
76+
# discriminant -ve: these particles do not collide.
77+
next [] if d < 0
78+
[-1, 1].map { |s| (-b + s * (d ** 0.5)) / (2 * a) }.reject(&:negative?)
79+
end
80+
}.compact
81+
82+
# If times.size == 0,
83+
# they collide at time 0 and do not affect simulation further.
84+
if times.size == 1
85+
max_t = [times.first, max_t].max
86+
elsif times.size > 0
87+
times[0].product(*times[1..-1]) { |xs|
88+
next unless xs.max == xs.min
89+
# Possible collision.
90+
max_t = [xs.max.ceil, max_t].max
91+
}
92+
end
93+
}
94+
95+
max_t.times {
96+
particles.each { |p, v, a|
97+
3.times { |i|
98+
v[i] += a[i]
99+
p[i] += v[i]
100+
}
101+
}
102+
particles = particles.group_by(&:first).select { |_, v|
103+
v.size == 1
104+
}.values.flatten(1)
105+
}
106+
107+
particles.size
108+
end
109+
110+
particles = ARGF.map { |l|
111+
l.scan(/-?\d+/).map(&method(:Integer)).each_slice(3).map(&:freeze).freeze
112+
}.freeze
113+
114+
results = {}
115+
116+
Benchmark.bmbm { |bm|
117+
bench_candidates.each { |f|
118+
bm.report(f) { 10.times { results[f] = send(f, particles.map { |p| p.map(&:dup) }) } }
119+
}
120+
}
121+
122+
# Obviously the benchmark would be useless if they got different answers.
123+
if results.values.uniq.size != 1
124+
results.each { |k, v| puts "#{k} #{v}" }
125+
raise 'differing answers'
126+
end
127+
128+
puts "slow (only run 1x instead of 10x!)"
129+
130+
Benchmark.bmbm { |bm|
131+
slow_bench_candidates.each { |f|
132+
bm.report(f) { 1.times { results[f] = send(f, particles.map { |p| p.map(&:dup) }) } }
133+
}
134+
}
135+
136+
# Obviously the benchmark would be useless if they got different answers.
137+
if results.values.uniq.size != 1
138+
results.each { |k, v| puts "#{k} #{v}" }
139+
raise 'differing answers'
140+
end

0 commit comments

Comments
 (0)