Ramp: red through yellow to white¶
A value's colour on the heat ramp, red at low through yellow to white at high, opaque: with t = clamp((value - low) / (high - low), 0, 1), the colour (1, clamp(2 t, 0, 1), clamp(2 t - 1, 0, 1), 1).
Generic as a float: any quantity, its bounds in its unit; an integer converts to a float first. A display kind of the specification, section 11. Source: lusine's ramp as lusine drew it before this kernel (lusine_ramp). Checked against a reference in numpy, which has its own tests.
- Source
- lusine's ramp as its GLSL back end lowered it (lusine_ramp).
- Proof
- limit, invariant, curve.
ramp.trait
namespace lusine.display;
vector<float, 4> ramp(float<_> value, float<_> low, float<_> high)
{
let t = clamp((value - low) / (high - low), 0.0, 1.0);
return vector<float, 4>(1.0, clamp(2.0 * t, 0.0, 1.0), clamp(2.0 * t - 1.0, 0.0, 1.0), 1.0);
}
A value's colour on the heat ramp, with t = clamp((value − low) / (high − low), 0, 1), the opaque colour (1, clamp(2t, 0, 1), clamp(2t − 1, 0, 1), 1). It is generic as a float: any quantity, its bounds in its unit; an integer, such as Life's age in steps, converts to a float first.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import trait_run
EPS = float(np.finfo(np.float32).eps) # Trait's float is binary32
The kernel¶
The law as the library states it.
print(trait_run.source('display/ramp.trait'))
// lusine.display.ramp: a value's colour on the heat ramp, red at low through yellow to white at high, opaque: with
// t = clamp((value - low) / (high - low), 0, 1), the colour (1, clamp(2 t, 0, 1), clamp(2 t - 1, 0, 1), 1). Generic as
// a float: any quantity, its bounds in its unit; an integer converts to a
// float first. A display kind of the specification, section 11.
// Source: lusine's ramp as lusine drew it before this kernel (lusine_ramp).
// Checked against a reference in numpy, which has its own tests.
namespace lusine.display;
vector<float, 4> ramp(float<_> value, float<_> low, float<_> high)
{
let t = clamp((value - low) / (high - low), 0.0, 1.0);
return vector<float, 4>(1.0, clamp(2.0 * t, 0.0, 1.0), clamp(2.0 * t - 1.0, 0.0, 1.0), 1.0);
}
The program¶
A lattice of cells, each coloured twice by the one kernel: by its temperature between a cold and a warm bound, and by its mass between a light and a heavy one.
SHADE = """namespace shade;
lusine.version version = { { number = 1 } };
struct cell { host float<K> temperature; host float<kg> mass; vector<float, 4> hot; vector<float, 4> heavy; };
lattice<cell, 1> cells(spacing = 1 m, boundary = wall);
host struct { float<K> cold; float<K> warm; float<kg> light; float<kg> massive; } settings;
void main()
{
cells.hot = lusine.display.ramp(value = cells.temperature, low = settings.cold, high = settings.warm);
cells.heavy = lusine.display.ramp(value = cells.mass, low = settings.light, high = settings.massive);
}
"""
print(SHADE)
def shade(temperature, mass, cold=0.0, warm=100.0, light=0.0, massive=100.0):
"""Each cell's colour on the ramp of its temperature and of its mass, red, green, blue, alpha the last axis."""
n = len(temperature)
run = trait_run.Program(['display/ramp.trait'], SHADE, {'shade.cells': [n]})
for k, v in dict(cold=cold, warm=warm, light=light, massive=massive).items():
run.store('shade.settings', k, v)
run.store('shade.cells', 'temperature', temperature)
run.store('shade.cells', 'mass', mass)
hot, heavy = run.steps('shade.main', 1, [('shade.cells', 'hot'), ('shade.cells', 'heavy')])
return hot[0], heavy[0]
namespace shade;
lusine.version version = { { number = 1 } };
struct cell { host float<K> temperature; host float<kg> mass; vector<float, 4> hot; vector<float, 4> heavy; };
lattice<cell, 1> cells(spacing = 1 m, boundary = wall);
host struct { float<K> cold; float<K> warm; float<kg> light; float<kg> massive; } settings;
void main()
{
cells.hot = lusine.display.ramp(value = cells.temperature, low = settings.cold, high = settings.warm);
cells.heavy = lusine.display.ramp(value = cells.mass, low = settings.light, high = settings.massive);
}
Limit: the ends and beyond¶
At the low bound red, halfway yellow, at the high bound white, each exactly; below the range red and above it white, the fraction clamped.
hot, heavy = shade([0.0, 50.0, 100.0, -40.0, 1e6], [0.0, 50.0, 100.0, -40.0, 1e6])
want = [[1, 0, 0, 1], [1, 1, 0, 1], [1, 1, 1, 1], [1, 0, 0, 1], [1, 1, 1, 1]]
assert np.array_equal(hot, want) and np.array_equal(heavy, want), hot
print(hot)
[[1. 0. 0. 1.] [1. 1. 0. 1.] [1. 1. 1. 1.] [1. 0. 0. 1.] [1. 1. 1. 1.]]
Invariant: generic, and monotone¶
A temperature between 200 K and 300 K and a mass between 0 kg and 100 kg at the same fraction of their ranges are the same colour, bit for bit: the unit is the caller's. Along a rising value green and blue never fall, blue rises only once green is full, and red and alpha stay 1.
x = np.linspace(-20.0, 120.0, 281)
hot, heavy = shade(200.0 + x, x, cold=200.0, warm=300.0)
assert np.array_equal(hot, heavy)
assert np.all(np.diff(heavy[:, 1]) >= 0) and np.all(np.diff(heavy[:, 2]) >= 0)
assert np.all(heavy[heavy[:, 2] > 0, 1] == 1.0)
assert np.all(heavy[:, 0] == 1.0) and np.all(heavy[:, 3] == 1.0)
print('one colour in kelvin and in kilograms; monotone')
one colour in kelvin and in kilograms; monotone
Curve: Life's ages¶
Life colours a live cell by its age, 0 to 255 steps converted to a float, between 0 and 100 steps: green is 2t and blue 2t − 1, each clamped, to binary32's rounding of t = age / 100; at the quarters every value is exact. The strip below is the colour by age.
age = np.arange(256, dtype=float)
colour, _ = shade(age, age)
t = np.minimum(age / 100.0, 1.0)
assert np.all(np.abs(colour[:, 1] - np.minimum(2 * t, 1)) <= 2 * EPS)
assert np.all(np.abs(colour[:, 2] - np.clip(2 * t - 1, 0, 1)) <= 2 * EPS)
assert np.array_equal(colour[[0, 25, 50, 75, 100], 1:3], [[0, 0], [0.5, 0], [1, 0], [1, 0.5], [1, 1]])
fig, (ax, strip) = plt.subplots(2, 1, figsize=(7, 3.6), height_ratios=[3, 1], sharex=True)
ax.plot(age, colour[:, 1], color='tab:green', label='green')
ax.plot(age, colour[:, 2], color='tab:blue', label='blue')
ax.set_ylabel('channel')
ax.legend()
strip.imshow(colour[None, :, :3], aspect='auto', extent=(0, 255, 0, 1))
strip.set_yticks([])
strip.set_xlabel('age (steps)')
plt.show()