carray-jit 0.1.2 → 0.1.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +771 -3
- data/README.md +7 -6
- data/carray-jit.gemspec +1 -3
- data/docs/00_Introduction.md +4 -3
- data/docs/01_GettingStarted.md +1 -1
- data/docs/02_KernelShapes.md +93 -14
- data/docs/03_SupportedFeatures.md +582 -26
- data/docs/04_Compiling.md +33 -6
- data/docs/05_DesignNotes.md +3 -3
- data/docs/06_Cheatsheet.md +198 -5
- data/docs/07_StepByStep.ja.md +534 -0
- data/docs/07_StepByStep.md +535 -0
- data/examples/README.md +12 -0
- data/examples/applications/alarm.rb +121 -0
- data/examples/applications/collatz.rb +105 -0
- data/examples/applications/cubic_spline.rb +331 -0
- data/examples/applications/dithering.rb +144 -0
- data/examples/applications/group_stats.rb +115 -0
- data/examples/applications/lookup.rb +126 -0
- data/examples/applications/median_filter.rb +153 -0
- data/examples/applications/parcel_ascent.rb +220 -0
- data/examples/applications/point_in_polygon.rb +111 -0
- data/examples/applications/random_walk.rb +98 -0
- data/examples/applications/van_der_pol.rb +186 -0
- data/examples/applications/wet_bulb.rb +140 -0
- data/examples/features/10_complex.rb +14 -4
- data/examples/features/15_loops.rb +7 -1
- data/lib/carray/jit/access.rb +14 -0
- data/lib/carray/jit/analyzer.rb +2077 -136
- data/lib/carray/jit/block_reader.rb +37 -6
- data/lib/carray/jit/c_function.rb +613 -76
- data/lib/carray/jit/c_generator.rb +1595 -156
- data/lib/carray/jit/call.rb +68 -0
- data/lib/carray/jit/compiler.rb +75 -11
- data/lib/carray/jit/kernel.rb +369 -32
- data/lib/carray/jit/node.rb +359 -9
- data/lib/carray/jit/sorting_networks.rb +182 -0
- data/lib/carray/jit/type_assignment.rb +371 -34
- data/lib/carray/jit/version.rb +1 -1
- data/lib/carray/jit.rb +560 -64
- metadata +22 -8
- data/ext/carray_jit_access/carray_jit_access.c +0 -460
- data/ext/carray_jit_access/extconf.rb +0 -8
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Gambler's ruin: how long a random walk lasts, and which end it stops at.
|
|
2
|
+
#
|
|
3
|
+
# A walk steps up or down by one until it reaches -A or +B. How many steps
|
|
4
|
+
# that takes is different every time and is not bounded by anything, so this
|
|
5
|
+
# is `while` again -- but the new part is where the steps come from. A kernel
|
|
6
|
+
# runs with the GVL released, so Ruby's `rand` is not reachable from inside
|
|
7
|
+
# one; what is reachable is `CArray::Rng`, whose C the compiler pastes into
|
|
8
|
+
# the kernel. One draw per step, in the loop, at no call.
|
|
9
|
+
#
|
|
10
|
+
# The answers are known exactly, which is what makes this worth running: a
|
|
11
|
+
# walk started at 0 between -A and +B lasts A*B steps on average and stops at
|
|
12
|
+
# +B with probability A/(A+B). A hundred thousand walks should land near
|
|
13
|
+
# both.
|
|
14
|
+
#
|
|
15
|
+
# ruby examples/applications/random_walk.rb
|
|
16
|
+
|
|
17
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
18
|
+
require "carray/jit"
|
|
19
|
+
|
|
20
|
+
WALKS = 200_000
|
|
21
|
+
DOWN, UP = -8, 12 # the two barriers
|
|
22
|
+
|
|
23
|
+
def walk (n, seed)
|
|
24
|
+
random = CArray::Rng.new(seed: seed)
|
|
25
|
+
steps = CArray.int32(n)
|
|
26
|
+
ended = CArray.int8(n) # 1 when it stopped at the top
|
|
27
|
+
CArray.jit_for(n) { |i|
|
|
28
|
+
position = 0
|
|
29
|
+
taken = 0
|
|
30
|
+
while position > DOWN && position < UP
|
|
31
|
+
position = random.random < 0.5 ? position - 1 : position + 1
|
|
32
|
+
taken = taken + 1
|
|
33
|
+
end
|
|
34
|
+
steps[i] = taken
|
|
35
|
+
ended[i] = position == UP ? 1 : 0
|
|
36
|
+
}
|
|
37
|
+
[steps, ended]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
steps, ended = walk(WALKS, 20260913)
|
|
41
|
+
|
|
42
|
+
puts format("%d walks between %d and %d", WALKS, DOWN, UP)
|
|
43
|
+
puts format(" mean length %8.2f (exactly %d)",
|
|
44
|
+
steps.mean, -DOWN * UP)
|
|
45
|
+
puts format(" stopped at the top %8.4f (exactly %.4f)",
|
|
46
|
+
ended.sum.to_f / WALKS, -DOWN.to_f / (UP - DOWN))
|
|
47
|
+
puts format(" longest walk %8d steps", steps.max)
|
|
48
|
+
|
|
49
|
+
# The shape of it, which no formula on the line above shows.
|
|
50
|
+
puts
|
|
51
|
+
puts "how long they lasted"
|
|
52
|
+
edges = [0, 25, 50, 100, 200, 400, 800, 1 << 30]
|
|
53
|
+
edges.each_cons(2) do |low, high|
|
|
54
|
+
count = steps.ge(low).and(steps.lt(high)).count(1)
|
|
55
|
+
width = 60 * count / WALKS
|
|
56
|
+
bar = "#" * (count.positive? ? [width, 1].max : 0)
|
|
57
|
+
label = high > WALKS ? format("%4d+", low) : format("%4d-%-4d", low, high - 1)
|
|
58
|
+
puts format(" %-10s %6d %s", label, count, bar)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A draw is not fixed to a cell. Which draw a cell gets is the order the loop
|
|
62
|
+
# ran in, so the same seed through the same kernel repeats exactly -- and if a
|
|
63
|
+
# cell has to get *its own* draw whatever the loop does, the way to say that
|
|
64
|
+
# is to fill an array with `CArray#random!` first and read a cell of it.
|
|
65
|
+
again, = walk(WALKS, 20260913)
|
|
66
|
+
puts
|
|
67
|
+
puts format("the same seed gives the same walks %s", steps.to_a == again.to_a)
|
|
68
|
+
|
|
69
|
+
# The same thing in Ruby, which is where a Monte Carlo usually starts life.
|
|
70
|
+
def walk_in_ruby (n, seed)
|
|
71
|
+
random = Random.new(seed)
|
|
72
|
+
steps = Array.new(n, 0)
|
|
73
|
+
n.times do |i|
|
|
74
|
+
position = 0
|
|
75
|
+
taken = 0
|
|
76
|
+
while position > DOWN && position < UP
|
|
77
|
+
position = random.rand < 0.5 ? position - 1 : position + 1
|
|
78
|
+
taken += 1
|
|
79
|
+
end
|
|
80
|
+
steps[i] = taken
|
|
81
|
+
end
|
|
82
|
+
steps
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
86
|
+
3.times { walk(WALKS, 20260913) }
|
|
87
|
+
compiled = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 3
|
|
88
|
+
|
|
89
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
+
in_ruby = walk_in_ruby(WALKS, 20260913)
|
|
91
|
+
interpreted = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
92
|
+
|
|
93
|
+
puts
|
|
94
|
+
puts format("%d walks, about %d draws in all", WALKS, steps.sum)
|
|
95
|
+
puts format(" %6.1f ms compiled, %6.0f ms in Ruby (%.0fx)",
|
|
96
|
+
compiled * 1e3, interpreted * 1e3, interpreted / compiled)
|
|
97
|
+
puts format(" Ruby's own walks average %.2f steps, which is the same law",
|
|
98
|
+
in_ruby.sum.to_f / WALKS)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# An adaptive integrator, where the step size is the trajectory's own.
|
|
2
|
+
#
|
|
3
|
+
# The van der Pol oscillator is stiff in proportion to its parameter: at
|
|
4
|
+
# mu = 0.1 it is a gentle circle, at mu = 40 it spends most of the cycle
|
|
5
|
+
# crawling and the rest of it snapping across. An integrator worth using
|
|
6
|
+
# answers that by changing its step -- taking it as long as the error allows,
|
|
7
|
+
# shortening it and *doing the step again* when the estimate comes back too
|
|
8
|
+
# large.
|
|
9
|
+
#
|
|
10
|
+
# Neither half of that survives an expression over whole arrays. A fixed step
|
|
11
|
+
# can be written as an array pass, but then every trajectory pays the stiffest
|
|
12
|
+
# one's step count; and a rejected step -- this trajectory redoing what it
|
|
13
|
+
# just did while the others move on -- has no array spelling at all. Here the
|
|
14
|
+
# step, the retry and the count belong to the cell.
|
|
15
|
+
#
|
|
16
|
+
# The method is Bogacki-Shampine 3(2): four stages, the third-order answer
|
|
17
|
+
# carried forward and the second-order one used to size the error.
|
|
18
|
+
#
|
|
19
|
+
# ruby examples/applications/van_der_pol.rb
|
|
20
|
+
|
|
21
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
22
|
+
require "carray/jit"
|
|
23
|
+
|
|
24
|
+
TRAJECTORIES = 2_000
|
|
25
|
+
FINISH = 20.0
|
|
26
|
+
TOLERANCE = 1.0e-8
|
|
27
|
+
CAP = 200_000 # a `while` in a kernel carries a bound
|
|
28
|
+
|
|
29
|
+
stiffness = CArray.double(TRAJECTORIES).seq!(0.1, 40.0 / TRAJECTORIES)
|
|
30
|
+
position = CArray.double(TRAJECTORIES)
|
|
31
|
+
velocity = CArray.double(TRAJECTORIES)
|
|
32
|
+
accepted = CArray.int32(TRAJECTORIES)
|
|
33
|
+
rejected = CArray.int32(TRAJECTORIES)
|
|
34
|
+
|
|
35
|
+
def integrate (stiffness, position, velocity, accepted, rejected)
|
|
36
|
+
CArray.jit_for(stiffness.elements) { |i|
|
|
37
|
+
mu = stiffness[i]
|
|
38
|
+
x = 2.0 # the trajectory starts out here
|
|
39
|
+
v = 0.0
|
|
40
|
+
t = 0.0
|
|
41
|
+
step = 0.01
|
|
42
|
+
taken = 0
|
|
43
|
+
thrown = 0
|
|
44
|
+
while t < FINISH && taken < CAP
|
|
45
|
+
step = FINISH - t if t + step > FINISH
|
|
46
|
+
|
|
47
|
+
k1x = v
|
|
48
|
+
k1v = mu * (1.0 - x * x) * v - x
|
|
49
|
+
x2 = x + 0.5 * step * k1x
|
|
50
|
+
v2 = v + 0.5 * step * k1v
|
|
51
|
+
k2x = v2
|
|
52
|
+
k2v = mu * (1.0 - x2 * x2) * v2 - x2
|
|
53
|
+
x3 = x + 0.75 * step * k2x
|
|
54
|
+
v3 = v + 0.75 * step * k2v
|
|
55
|
+
k3x = v3
|
|
56
|
+
k3v = mu * (1.0 - x3 * x3) * v3 - x3
|
|
57
|
+
nx = x + step * (2.0 * k1x + 3.0 * k2x + 4.0 * k3x) / 9.0
|
|
58
|
+
nv = v + step * (2.0 * k1v + 3.0 * k2v + 4.0 * k3v) / 9.0
|
|
59
|
+
k4x = nv
|
|
60
|
+
k4v = mu * (1.0 - nx * nx) * nv - nx
|
|
61
|
+
|
|
62
|
+
ex = step * (-5.0 * k1x / 72.0 + k2x / 12.0 + k3x / 9.0 - k4x / 8.0)
|
|
63
|
+
ev = step * (-5.0 * k1v / 72.0 + k2v / 12.0 + k3v / 9.0 - k4v / 8.0)
|
|
64
|
+
error = ex.abs > ev.abs ? ex.abs : ev.abs
|
|
65
|
+
allowed = TOLERANCE * (1.0 + (nx.abs > nv.abs ? nx.abs : nv.abs))
|
|
66
|
+
|
|
67
|
+
if error <= allowed
|
|
68
|
+
t = t + step
|
|
69
|
+
x = nx
|
|
70
|
+
v = nv
|
|
71
|
+
taken = taken + 1
|
|
72
|
+
else
|
|
73
|
+
# The step is not taken: this trajectory does it again, shorter,
|
|
74
|
+
# while every other one carries on from wherever it is.
|
|
75
|
+
thrown = thrown + 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
growth = 0.9 * Math.exp(Math.log(allowed / (error + 1.0e-300)) / 3.0)
|
|
79
|
+
growth = 5.0 if growth > 5.0
|
|
80
|
+
growth = 0.2 if growth < 0.2
|
|
81
|
+
step = step * growth
|
|
82
|
+
end
|
|
83
|
+
position[i] = x
|
|
84
|
+
velocity[i] = v
|
|
85
|
+
accepted[i] = taken
|
|
86
|
+
rejected[i] = thrown
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
integrate(stiffness, position, velocity, accepted, rejected)
|
|
91
|
+
|
|
92
|
+
puts format("%d trajectories, mu from %.1f to %.1f, integrated to t = %.0f",
|
|
93
|
+
TRAJECTORIES, stiffness[0], stiffness[TRAJECTORIES - 1], FINISH)
|
|
94
|
+
puts format(" steps taken %d at the least, %d at the most, %.0f on average",
|
|
95
|
+
accepted.min, accepted.max, accepted.mean)
|
|
96
|
+
puts format(" steps thrown away up to %d on one trajectory", rejected.max)
|
|
97
|
+
puts format(" a fixed step would have to be the stiffest one's, so %d for everybody",
|
|
98
|
+
accepted.max)
|
|
99
|
+
puts format(" which is %.1f times the work this did", accepted.max.to_f / accepted.mean)
|
|
100
|
+
|
|
101
|
+
# Against a reference: the same trajectory by fixed-step RK4, fine enough that
|
|
102
|
+
# it is the answer rather than another approximation.
|
|
103
|
+
def reference (mu, finish, steps)
|
|
104
|
+
x = 2.0
|
|
105
|
+
v = 0.0
|
|
106
|
+
step = finish / steps
|
|
107
|
+
steps.times do
|
|
108
|
+
k1x = v; k1v = mu * (1.0 - x * x) * v - x
|
|
109
|
+
ax = x + 0.5 * step * k1x; av = v + 0.5 * step * k1v
|
|
110
|
+
k2x = av; k2v = mu * (1.0 - ax * ax) * av - ax
|
|
111
|
+
bx = x + 0.5 * step * k2x; bv = v + 0.5 * step * k2v
|
|
112
|
+
k3x = bv; k3v = mu * (1.0 - bx * bx) * bv - bx
|
|
113
|
+
cx = x + step * k3x; cv = v + step * k3v
|
|
114
|
+
k4x = cv; k4v = mu * (1.0 - cx * cx) * cv - cx
|
|
115
|
+
x += step * (k1x + 2 * k2x + 2 * k3x + k4x) / 6.0
|
|
116
|
+
v += step * (k1v + 2 * k2v + 2 * k3v + k4v) / 6.0
|
|
117
|
+
end
|
|
118
|
+
x
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
puts
|
|
122
|
+
puts " mu here reference diff steps"
|
|
123
|
+
[0, TRAJECTORIES / 2, TRAJECTORIES - 1].each do |i|
|
|
124
|
+
exact = reference(stiffness[i], FINISH, 200_000)
|
|
125
|
+
puts format(" %5.1f %8.5f %10.5f %10.2e %6d",
|
|
126
|
+
stiffness[i], position[i], exact, (position[i] - exact).abs, accepted[i])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Ruby does the same adaptive integration, over as many trajectories as is
|
|
130
|
+
# polite to wait for.
|
|
131
|
+
SAMPLE = 100
|
|
132
|
+
|
|
133
|
+
def integrate_in_ruby (stiffness, count)
|
|
134
|
+
count.times.map do |i|
|
|
135
|
+
mu = stiffness[i]
|
|
136
|
+
x = 2.0
|
|
137
|
+
v = 0.0
|
|
138
|
+
t = 0.0
|
|
139
|
+
step = 0.01
|
|
140
|
+
taken = 0
|
|
141
|
+
while t < FINISH && taken < CAP
|
|
142
|
+
step = FINISH - t if t + step > FINISH
|
|
143
|
+
k1x = v; k1v = mu * (1.0 - x * x) * v - x
|
|
144
|
+
x2 = x + 0.5 * step * k1x; v2 = v + 0.5 * step * k1v
|
|
145
|
+
k2x = v2; k2v = mu * (1.0 - x2 * x2) * v2 - x2
|
|
146
|
+
x3 = x + 0.75 * step * k2x; v3 = v + 0.75 * step * k2v
|
|
147
|
+
k3x = v3; k3v = mu * (1.0 - x3 * x3) * v3 - x3
|
|
148
|
+
nx = x + step * (2.0 * k1x + 3.0 * k2x + 4.0 * k3x) / 9.0
|
|
149
|
+
nv = v + step * (2.0 * k1v + 3.0 * k2v + 4.0 * k3v) / 9.0
|
|
150
|
+
k4x = nv; k4v = mu * (1.0 - nx * nx) * nv - nx
|
|
151
|
+
ex = step * (-5.0 * k1x / 72.0 + k2x / 12.0 + k3x / 9.0 - k4x / 8.0)
|
|
152
|
+
ev = step * (-5.0 * k1v / 72.0 + k2v / 12.0 + k3v / 9.0 - k4v / 8.0)
|
|
153
|
+
error = [ex.abs, ev.abs].max
|
|
154
|
+
allowed = TOLERANCE * (1.0 + [nx.abs, nv.abs].max)
|
|
155
|
+
if error <= allowed
|
|
156
|
+
t += step
|
|
157
|
+
x = nx
|
|
158
|
+
v = nv
|
|
159
|
+
taken += 1
|
|
160
|
+
end
|
|
161
|
+
growth = 0.9 * (allowed / (error + 1.0e-300)) ** (1.0 / 3.0)
|
|
162
|
+
growth = 5.0 if growth > 5.0
|
|
163
|
+
growth = 0.2 if growth < 0.2
|
|
164
|
+
step *= growth
|
|
165
|
+
end
|
|
166
|
+
x
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
in_ruby = integrate_in_ruby(stiffness, SAMPLE)
|
|
171
|
+
agree = (0...SAMPLE).all? { |i| (position[i] - in_ruby[i]).abs < 1.0e-12 }
|
|
172
|
+
|
|
173
|
+
def timed (repeats = 3)
|
|
174
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
175
|
+
repeats.times { yield }
|
|
176
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
compiled = timed { integrate(stiffness, position, velocity, accepted, rejected) }
|
|
180
|
+
interpreted = timed(1) { integrate_in_ruby(stiffness, SAMPLE) }
|
|
181
|
+
|
|
182
|
+
puts
|
|
183
|
+
puts format(" agrees with the same integrator in Ruby %s", agree)
|
|
184
|
+
puts format(" %6.0f ms here for %d trajectories, %6.0f ms in Ruby for %d (%.0fx a trajectory)",
|
|
185
|
+
compiled * 1e3, TRAJECTORIES, interpreted * 1e3, SAMPLE,
|
|
186
|
+
(interpreted / SAMPLE) / (compiled / TRAJECTORIES))
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# A derived field on a weather grid, where one of the fields needs solving.
|
|
2
|
+
#
|
|
3
|
+
# Temperature and relative humidity come off a grid -- a GRIB message, a
|
|
4
|
+
# netCDF variable, a model's output -- and what is wanted is something else.
|
|
5
|
+
# The dew point is an expression: saturation vapour pressure, the actual
|
|
6
|
+
# vapour pressure from the humidity, and Magnus inverted. Written over whole
|
|
7
|
+
# arrays that is four passes and three temporaries the size of the grid;
|
|
8
|
+
# written as a kernel it is one pass and none, which is the ordinary reason
|
|
9
|
+
# to use one.
|
|
10
|
+
#
|
|
11
|
+
# The wet-bulb temperature is the other kind. It is defined by the
|
|
12
|
+
# psychrometric equation, which has no closed form, so every cell solves its
|
|
13
|
+
# own -- and how many Newton steps that takes depends on how far the cell's
|
|
14
|
+
# dew point is from its answer, which is the cell's own business. The
|
|
15
|
+
# whole-array route can still be written, because a fixed number of rounds
|
|
16
|
+
# covers the worst cell; what it cannot do is let a cell that has converged
|
|
17
|
+
# stop, so every cell pays the worst one's count, and each round is a pass
|
|
18
|
+
# over the grid holding another grid.
|
|
19
|
+
#
|
|
20
|
+
# ruby examples/applications/wet_bulb.rb
|
|
21
|
+
|
|
22
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
23
|
+
require "carray/jit"
|
|
24
|
+
|
|
25
|
+
ROWS, COLUMNS = 721, 1440 # a quarter-degree global grid
|
|
26
|
+
PRESSURE = 1013.25 # hPa, one level
|
|
27
|
+
TOLERANCE = 1.0e-6 # degrees, how close is close enough
|
|
28
|
+
CAP = 50 # a `while` in a kernel takes a cap
|
|
29
|
+
|
|
30
|
+
# Stand-ins for the fields that would have been read: a pole-to-pole
|
|
31
|
+
# temperature gradient with a wave in it, and humidity that varies across it.
|
|
32
|
+
temperature = CArray.double(ROWS, COLUMNS)
|
|
33
|
+
humidity = CArray.double(ROWS, COLUMNS)
|
|
34
|
+
CArray.jit_for(ROWS, COLUMNS) { |j, i|
|
|
35
|
+
latitude = 90.0 - 180.0 * j / (ROWS - 1)
|
|
36
|
+
temperature[j, i] = 30.0 - 55.0 * (latitude / 90.0).abs +
|
|
37
|
+
4.0 * Math.sin(i / 40.0) * Math.cos(latitude / 25.0)
|
|
38
|
+
humidity[j, i] = 35.0 + 60.0 * (0.5 + 0.5 * Math.cos(i / 30.0 + j / 60.0))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
dew_point = CArray.double(ROWS, COLUMNS)
|
|
42
|
+
wet_bulb = CArray.double(ROWS, COLUMNS)
|
|
43
|
+
rounds_taken = CArray.int8(ROWS, COLUMNS)
|
|
44
|
+
|
|
45
|
+
# Everything in one walk. `vapour` and `gamma` are the cell's own values and
|
|
46
|
+
# never become arrays; `low` and `high` are the cell's bracket, which is why
|
|
47
|
+
# the loop can end when this cell is done rather than when the last one is.
|
|
48
|
+
def derive (temperature, humidity, dew_point, wet_bulb, rounds_taken)
|
|
49
|
+
rows, columns = temperature.dim
|
|
50
|
+
CArray.jit_for(rows, columns) { |j, i|
|
|
51
|
+
air = temperature[j, i]
|
|
52
|
+
saturation = 6.112 * Math.exp(17.67 * air / (air + 243.5))
|
|
53
|
+
vapour = saturation * humidity[j, i] / 100.0
|
|
54
|
+
gamma = Math.log(vapour / 6.112)
|
|
55
|
+
dew = 243.5 * gamma / (17.67 - gamma)
|
|
56
|
+
dew_point[j, i] = dew
|
|
57
|
+
|
|
58
|
+
# Newton on the psychrometric equation, started from the dew point. The
|
|
59
|
+
# cap is not decoration: a `while` in a kernel has no interpreter to
|
|
60
|
+
# interrupt it, so a solver carries the bound it is allowed.
|
|
61
|
+
guess = dew
|
|
62
|
+
step = 1.0
|
|
63
|
+
taken = 0
|
|
64
|
+
while step.abs > TOLERANCE && taken < CAP
|
|
65
|
+
saturated = 6.112 * Math.exp(17.67 * guess / (guess + 243.5))
|
|
66
|
+
residual = saturated - 0.000665 * PRESSURE * (air - guess) - vapour
|
|
67
|
+
slope = saturated * 17.67 * 243.5 / ((guess + 243.5) * (guess + 243.5)) +
|
|
68
|
+
0.000665 * PRESSURE
|
|
69
|
+
step = residual / slope
|
|
70
|
+
guess = guess - step
|
|
71
|
+
taken = taken + 1
|
|
72
|
+
end
|
|
73
|
+
wet_bulb[j, i] = guess
|
|
74
|
+
rounds_taken[j, i] = taken
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
derive(temperature, humidity, dew_point, wet_bulb, rounds_taken)
|
|
79
|
+
|
|
80
|
+
puts format("%d x %d grid, %d cells", ROWS, COLUMNS, ROWS * COLUMNS)
|
|
81
|
+
puts " T RH Td Tw"
|
|
82
|
+
[[180, 0], [360, 200], [360, 720], [540, 1000], [700, 30]].each do |j, i|
|
|
83
|
+
puts format(" %6.2f %6.1f %8.2f %8.2f",
|
|
84
|
+
temperature[j, i], humidity[j, i], dew_point[j, i], wet_bulb[j, i])
|
|
85
|
+
end
|
|
86
|
+
puts format(" the wet bulb is never above the air temperature %s",
|
|
87
|
+
(wet_bulb - temperature).max <= 0.0)
|
|
88
|
+
puts format(" and never below the dew point %s",
|
|
89
|
+
(wet_bulb - dew_point).min >= -TOLERANCE)
|
|
90
|
+
|
|
91
|
+
# The part an expression cannot have: the cells did not agree on how long
|
|
92
|
+
# this took.
|
|
93
|
+
puts
|
|
94
|
+
puts format("Newton steps per cell: %d at the least, %d at the most, %.2f on average",
|
|
95
|
+
rounds_taken.min, rounds_taken.max, rounds_taken.mean)
|
|
96
|
+
|
|
97
|
+
# The same Newton iteration over whole arrays. It cannot stop per cell, so it runs
|
|
98
|
+
# the worst cell's count everywhere, and every line here is a pass over the
|
|
99
|
+
# grid holding another grid.
|
|
100
|
+
def derive_over_arrays (temperature, humidity, rounds)
|
|
101
|
+
saturation = (temperature * 17.67 / (temperature + 243.5)).exp * 6.112
|
|
102
|
+
vapour = saturation * humidity / 100.0
|
|
103
|
+
gamma = (vapour / 6.112).log
|
|
104
|
+
dew = gamma * 243.5 / (17.67 - gamma)
|
|
105
|
+
guess = dew.to_ca
|
|
106
|
+
rounds.times do
|
|
107
|
+
saturated = (guess * 17.67 / (guess + 243.5)).exp * 6.112
|
|
108
|
+
residual = saturated - (temperature - guess) * (0.000665 * PRESSURE) - vapour
|
|
109
|
+
slope = saturated * (17.67 * 243.5) / ((guess + 243.5) * (guess + 243.5)) +
|
|
110
|
+
0.000665 * PRESSURE
|
|
111
|
+
guess = guess - residual / slope
|
|
112
|
+
end
|
|
113
|
+
[dew, guess]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
rounds = rounds_taken.max
|
|
117
|
+
array_dew, array_wet = derive_over_arrays(temperature, humidity, rounds)
|
|
118
|
+
puts format(" over whole arrays, everyone pays %d rounds", rounds)
|
|
119
|
+
puts format(" and agrees with the kernel to %.1e %s", TOLERANCE,
|
|
120
|
+
(array_wet - wet_bulb).abs.max < TOLERANCE)
|
|
121
|
+
|
|
122
|
+
def timed (repeats = 3)
|
|
123
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
124
|
+
repeats.times { yield }
|
|
125
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / repeats
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
compiled = timed { derive(temperature, humidity, dew_point, wet_bulb, rounds_taken) }
|
|
129
|
+
whole = timed { derive_over_arrays(temperature, humidity, rounds) }
|
|
130
|
+
|
|
131
|
+
puts
|
|
132
|
+
puts format(" one kernel %7.0f ms", compiled * 1e3)
|
|
133
|
+
puts format(" whole arrays %7.0f ms %.1fx", whole * 1e3, whole / compiled)
|
|
134
|
+
puts format(" %.2f Newton steps a cell here, %d for every cell there", rounds_taken.mean, rounds)
|
|
135
|
+
# Each array operation in `derive_over_arrays` is its own walk over the grid
|
|
136
|
+
# and its own array to hold the answer -- sixteen of them per round, counted
|
|
137
|
+
# off the expression -- where the kernel walks the grid once and holds the
|
|
138
|
+
# cell's `guess` in a register.
|
|
139
|
+
puts format(" the grid is %.1f MB, and the array route builds %d of those and walks the grid %d times",
|
|
140
|
+
ROWS * COLUMNS * 8 / 1048576.0, 16 * rounds, 16 * rounds)
|
|
@@ -48,15 +48,25 @@ CArray.jit_for(n) { |i| transformed[i] = signal[i].tanh }
|
|
|
48
48
|
puts " tanh matches the array operator #{transformed.to_a == signal.tanh.to_a}"
|
|
49
49
|
|
|
50
50
|
# A reduction over complex cells: the accumulator starts from a complex zero.
|
|
51
|
+
# The sum is split into partial ones, as every reduction here is, so the last
|
|
52
|
+
# bits are not the serial loop's -- `reassociate: false` asks for that order
|
|
53
|
+
# and gets it exactly.
|
|
51
54
|
total = CArray.cmplx128(1)
|
|
52
55
|
CArray.jit_for(1) { |i|
|
|
53
56
|
running = Complex(0.0, 0.0)
|
|
54
57
|
(0...n).each { |j| running = running + signal[j] }
|
|
55
58
|
total[i] = running
|
|
56
59
|
}
|
|
60
|
+
in_order = CArray.cmplx128(1)
|
|
61
|
+
CArray.jit_for(1, reassociate: false) { |i|
|
|
62
|
+
running = Complex(0.0, 0.0)
|
|
63
|
+
(0...n).each { |j| running = running + signal[j] }
|
|
64
|
+
in_order[i] = running
|
|
65
|
+
}
|
|
57
66
|
serial = (0...n).inject(Complex(0.0, 0.0)) { |sum, j| sum + signal[j] }
|
|
58
67
|
puts " sum #{total[0].rectangular.map { |e| e.round(6) }.inspect}"
|
|
59
|
-
puts " matches Ruby #{total[0] == serial}"
|
|
68
|
+
puts " split sum matches Ruby #{total[0] == serial} <- the partial sums"
|
|
69
|
+
puts " serial sum matches Ruby #{in_order[0] == serial} <- reassociate: false"
|
|
60
70
|
|
|
61
71
|
# Where Ruby and C part company. A real operand carries an exact Integer zero
|
|
62
72
|
# as its imaginary part, and Ruby's own arithmetic returns the other operand
|
|
@@ -72,8 +82,8 @@ puts " Ruby #{(Complex(1.0, -0.0) + 2.0)}"
|
|
|
72
82
|
puts " the kernel #{kept[0]}"
|
|
73
83
|
puts " widening first #{Complex(1.0, -0.0) + Complex(2.0, 0.0)} <- what C's + would give"
|
|
74
84
|
|
|
75
|
-
# `**` is the one operation whose answer is
|
|
76
|
-
# bit: Ruby raises a Complex to a power by binary powering, cpow goes round
|
|
85
|
+
# Once the order is the same, `**` is the one operation whose answer is still
|
|
86
|
+
# not the Ruby loop's to the last bit: Ruby raises a Complex to a power by binary powering, cpow goes round
|
|
77
87
|
# through exp and log. A few machine epsilons apart, growing with the
|
|
78
88
|
# exponent -- and `z * z` is both exact and cheaper than the library call.
|
|
79
89
|
cubed = CArray.cmplx128(n)
|
|
@@ -84,7 +94,7 @@ squared = CArray.cmplx128(n)
|
|
|
84
94
|
CArray.jit_for(n) { |i| squared[i] = signal[i] * signal[i] }
|
|
85
95
|
|
|
86
96
|
puts
|
|
87
|
-
puts "the one inexact operation"
|
|
97
|
+
puts "the one inexact operation, the order being equal"
|
|
88
98
|
puts " z ** 3 worst #{(worst / Float::EPSILON).round(1)} machine epsilons"
|
|
89
99
|
puts " z * z matches Ruby exactly #{(0...n).all? { |i| squared[i] == signal[i] * signal[i] }}"
|
|
90
100
|
|
|
@@ -61,7 +61,13 @@ CArray.jit_for(n) { |i|
|
|
|
61
61
|
|
|
62
62
|
puts
|
|
63
63
|
puts "Newton, bounded by the extent"
|
|
64
|
-
|
|
64
|
+
# Against Math.sqrt rather than against the loop's own residual: the residual
|
|
65
|
+
# is what the break tested, so it can only come back at the tolerance.
|
|
66
|
+
exact = CArray.double(n) { |i| Math.sqrt(a[i]) }
|
|
67
|
+
puts format(" worst relative error %.1e (vs Math.sqrt)",
|
|
68
|
+
((root - exact).abs / exact).max)
|
|
69
|
+
puts format(" worst residual %.1e (what the break tested)",
|
|
70
|
+
((root ** 2 - a).abs / a).max)
|
|
65
71
|
puts " passes taken #{passes.min}..#{passes.max}"
|
|
66
72
|
puts " none ran out #{passes.max < cap}"
|
|
67
73
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The address basis a kernel is handed -- a pointer, and one byte stride per
|
|
2
|
+
# axis, lent for the length of a block -- is knowledge about CArray's views
|
|
3
|
+
# written against CArray's own C, and it lives there. This gem carried it as
|
|
4
|
+
# a second C extension until carray 3.0.2, which is why the old name is still
|
|
5
|
+
# here; it goes at the next minor.
|
|
6
|
+
#
|
|
7
|
+
# The file stays rather than the alias moving into `carray/jit.rb`, because
|
|
8
|
+
# this is what the light half loads. A program whose sites are all answered
|
|
9
|
+
# by a library built ahead of it never loads the compiler, and `CFunction`
|
|
10
|
+
# still has to be handed an address -- see `carray/jit/call`.
|
|
11
|
+
require "carray"
|
|
12
|
+
require "carray/jit/version"
|
|
13
|
+
|
|
14
|
+
CArray::JIT::Access = CArray::AddressBasis
|