Skip to content

The absorption coefficient, live

The attenuation coefficient of light in a phase, α = 4πk / λ, from its extinction coefficient k at a wavelength λ in vacuum: the intensity falls as e^(−αd) over a depth d (Beer and Lambert's law), and 1 / α is the depth over which it falls by e. Below, the library's law (lusine.properties.absorption) gives liquid water's α at 650, 550 and 450 nm from the library's own refractive index of water, on your GPU as you read, and the depth is swept from the surface to 20 m, an eighth of a metre a step, twenty steps a second, with the share of each colour left there. Beside it the program works the same intensity out from the wave itself: light of complex index n + ik carries a field whose amplitude decays as e^(−2πkd/λ), and the intensity is that amplitude squared.

This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.

What it shows

  • The strip above the controls is green while every check holds, and turns red for good once one fails.
  • Under water: the chart draws the share of red, green and blue light left against the depth, a point a step over one whole sweep: the red is nearly gone by 10 m while most of the blue is still there, which is why deep water is blue. Below it, the depth, and the red and blue left there beside the field's decay squared.
  • Falls by e over: the depths 1 / α for water's red and blue, soda-lime glass's green (its iron's tint, seen edge-on) and silicon's blue, which goes in under half a micrometre.
  • The checks, each held at every step: the depths, water's red falling by e over 3.154 m (within 2 mm) and its blue over 35.11 m (within 2 cm), glass's green over 0.1989 m and silicon's blue over 0.415 µm, from Hale and Querry's and the library's other measured k; water is blue, less than 5% of the red left at 10 m and more than 70% of the blue; the field squared, the intensity within 0.001% of the field's decay squared at every depth; transparent, a phase with k = 0 absorbing nothing; scales, twice k, or half the wavelength, giving exactly twice α.

Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The library's rows and the sweep are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The sweep, the law's answers and the checks, the panels the page draws, and the one procedure, main, run each step: the sweep moves on, the law is asked for each colour and phase, and the checks are taken.

// The absorption coefficient's live page: light of red, green and blue going down through liquid water, the library's
// absorption coefficient of each from the library's own refractive index, beside the square of the field's decay the
// complex index gives, with the page's checks computed here on the GPU and shown by the panels the program declares.

namespace absorption;

lusine.version version = { { number = 1 } };

// The reader's settings: liquid water's refractive index at 650, 550 and 450 nm, soda-lime glass's at 550 nm and
// silicon's at 450 nm, each a row of the library's; and how far each step moves the depth, from the surface.
host struct {
  relation<lusine.materials.refractive_index> red
    = lusine.materials.refractive_index[lusine.materials.substances["water"], "liquid", 650.0 nm];
  relation<lusine.materials.refractive_index> green
    = lusine.materials.refractive_index[lusine.materials.substances["water"], "liquid", 550.0 nm];
  relation<lusine.materials.refractive_index> blue
    = lusine.materials.refractive_index[lusine.materials.substances["water"], "liquid", 450.0 nm];
  relation<lusine.materials.refractive_index> glass
    = lusine.materials.refractive_index[lusine.materials.substances["soda_lime_glass"], "solid", 550.0 nm];
  relation<lusine.materials.refractive_index> silicon
    = lusine.materials.refractive_index[lusine.materials.substances["silicon"], "solid", 450.0 nm];
  float<m> stride = 0.125 m;
} settings;

// The sweep: the step it is at, from 0 to 159 and round again, and the depth there (to 20 m).
struct { integer step = -1; float<m> depth; } sweep;

// Each colour's absorption coefficient and the share of it left at the depth, exp(-alpha d); the field's decay the
// complex index n + ik gives, exp(-2 pi k d / lambda), squared for the intensity; and the depths over which red, blue,
// silicon's blue and glass's green fall by e, 1 / alpha.
struct {
  float<m^-1> red; float<m^-1> green; float<m^-1> blue; float red_left; float green_left; float blue_left;
  float red_field; float blue_field; float<m> red_depth; float<m> blue_depth; float<m> silicon_depth;
  float<m> glass_depth;
} law;

// The checks: water's red falling by e over 3.154 m and its blue over 35.11 m (Hale and Querry's k, the library's),
// silicon's blue over 0.415 um and glass's green over 0.1989 m, each within its tolerance; by 10 m under water less
// than 5% of the red left and more than 70% of the blue; the intensity the field's decay squared within 0.001% at every
// depth; a transparent phase, k = 0, absorbing nothing; and twice k, or half the wavelength, exactly twice alpha.
struct {
  bool tabulated = true; bool blue = true; bool field = true; bool transparent = true; bool scales = true;
} checks;

// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);

vector<float, 4> lamp_colour(bool holds)
{
  return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}

float<m^-1> alpha(relation<lusine.materials.refractive_index> index)
{
  return lusine.properties.absorption(k = index.k, wavelength = index.wavelength);
}

// The intensity left at a depth from the field's own decay: the wave exp(i 2 pi (n + ik) d / lambda) has amplitude
// exp(-2 pi k d / lambda), and the intensity is its square.
float field_squared(relation<lusine.materials.refractive_index> index, float<m> depth)
{
  let amplitude = exp;
  return amplitude * amplitude;
}

bool scales(relation<lusine.materials.refractive_index> index)
{
  let once = lusine.properties.absorption(k = index.k, wavelength = index.wavelength);
  return lusine.properties.absorption(k = 2.0 * index.k, wavelength = index.wavelength) == 2.0 * once
      && lusine.properties.absorption(k = index.k, wavelength = 0.5 * index.wavelength) == 2.0 * once;
}

bool agrees(float got, float want) { return abs(got - want) <= 0.00001 * want; }

output table<lusine.widget> panels = {
  lusine.series { { panel = "under water", chart = "light left", quantity = law.blue_left, steps = 160,
                    against = sweep.depth, label = "450 nm" } },
  lusine.series { { panel = "under water", chart = "light left", quantity = law.red_left, steps = 160,
                    against = sweep.depth, label = "650 nm" } },
  lusine.series { { panel = "under water", chart = "light left", quantity = law.green_left, steps = 160,
                    against = sweep.depth, label = "550 nm" } },
  lusine.value { { panel = "under water", quantity = sweep.depth, label = "depth" } },
  lusine.value { { panel = "under water", quantity = law.red_left, label = "red left" } },
  lusine.value { { panel = "under water", quantity = law.red_field, label = "red, the field squared" } },
  lusine.value { { panel = "under water", quantity = law.blue_left, label = "blue left" } },
  lusine.value { { panel = "under water", quantity = law.blue_field, label = "blue, the field squared" } },
  lusine.value { { panel = "falls by e over", quantity = law.red_depth, label = "water, 650 nm" } },
  lusine.value { { panel = "falls by e over", quantity = law.blue_depth, label = "water, 450 nm" } },
  lusine.value { { panel = "falls by e over", quantity = law.glass_depth, label = "soda-lime glass, 550 nm" } },
  lusine.value { { panel = "falls by e over", quantity = law.silicon_depth, label = "silicon, 450 nm" } },
  lusine.value { { panel = "the checks", quantity = checks.tabulated, label = "the depths" } },
  lusine.value { { panel = "the checks", quantity = checks.blue, label = "water is blue" } },
  lusine.value { { panel = "the checks", quantity = checks.field, label = "the field squared" } },
  lusine.value { { panel = "the checks", quantity = checks.transparent, label = "transparent" } },
  lusine.value { { panel = "the checks", quantity = checks.scales, label = "scales" } },
};

void main()
{
  sweep.step = sweep.step >= 159 ? 0 : sweep.step + 1;
  sweep.depth = float(sweep.step) * settings.stride;
  law.red = alpha(index = settings.red);
  law.green = alpha(index = settings.green);
  law.blue = alpha(index = settings.blue);
  law.red_left = exp(-law.red * sweep.depth);
  law.green_left = exp(-law.green * sweep.depth);
  law.blue_left = exp(-law.blue * sweep.depth);
  law.red_field = field_squared(index = settings.red, sweep.depth);
  law.blue_field = field_squared(index = settings.blue, sweep.depth);
  law.red_depth = 1.0 / law.red;
  law.blue_depth = 1.0 / law.blue;
  law.glass_depth = 1.0 / alpha(index = settings.glass);
  law.silicon_depth = 1.0 / alpha(index = settings.silicon);
  checks.tabulated = checks.tabulated && abs(law.red_depth - 3.154 m) < 0.002 m
                     && abs(law.blue_depth - 35.11 m) < 0.02 m && abs(law.glass_depth - 0.1989 m) < 0.001 m
                     && abs < 0.000000002 m;
  checks.blue = checks.blue && exp(-law.red * 10.0 m) < 0.05 && exp(-law.blue * 10.0 m) > 0.7;
  checks.field = checks.field && agrees(got = law.red_left, want = law.red_field)
                 && agrees(got = law.blue_left, want = law.blue_field);
  checks.transparent = checks.transparent
                       && lusine.properties.absorption(k = 0.0, wavelength = 0.00000055 m) == 0.0 m^-1;
  checks.scales = checks.scales && scales(index = settings.red) && scales(index = settings.blue);
  lamps.colour = lamp_colour(holds = checks.tabulated && checks.blue && checks.field && checks.transparent
                                     && checks.scales);
}

The law's other proof (every phase in the library, and αλ / k = 4π over phases at random) is on its proof page.