Skip to content

Optometry basics

The domain knowledge behind a prescription (Rx) and an Eye record: what the values mean, and why the matching algorithm is shaped the way it is. A pair only matches if both eyes fit at once, which is why every value below is per eye.

The four values per eye

Each eye (OD = right, OS = left) always uses 0.25 steps:

ValueMeaningRange
Spherebase lens power (diopters)-20 to +20, usually -6 to +6. Negative = near-sighted, positive = far-sighted
Cylindercorrects astigmatism0 to -6, usually 0 to -3. Zero or negative only
Axisorientation of the cylinder correction0 to 180 (0 and 180 are the same, it wraps around). Meaningless when cylinder is 0
Addextra plus power for reading, multifocal only+0.25 to +4, usually +0.75 to +3.50

Axis only makes sense together with cylinder

With no cylinder there's no orientation to have, so an axis value next to a zero cylinder is noise. REIMS2 enforces this in sanitizeEyeValues (frontend/src/util/eye-utils.ts), which forces axis to 0 whenever cylinder is 0. Code that reads axis values from elsewhere (raw analysis of dispensed glasses, say) should check cylinder first.

Power vectors: measuring "how far apart" two axes are

Axis also wraps at 180, so it isn't an ordinary number line. The standard fix, from the optometric literature, is to convert (sphere, cylinder, axis) into a power vector (Thibos, Wheeler & Horner, 1997):

M   = S + C/2                  spherical equivalent
J0  = -(C/2) * cos(2 * axis)   with-the-rule / against-the-rule component
J45 = -(C/2) * sin(2 * axis)   oblique component

Doubling the axis angle handles the 180 wraparound exactly, and the vector's length shrinks to 0 as cylinder does, matching the previous section. Euclidean distance in (M, J0, J45) space is optically meaningful in a way raw sphere/cylinder/axis distance isn't.

This is also the reasoning behind calcAxisTolerance's lookup table in philscore.ts. For two lenses with the same cylinder C but axes d degrees apart, the uncorrected astigmatism left over is:

residual astigmatism = 2 * |C| * sin(d)

The same angular error d leaves more uncorrected astigmatism as C grows, so axis tolerance has to shrink as cylinder grows too. REIMS2 doesn't compute power vectors at runtime; calcAxisTolerance's table is a hand-tuned approximation of a roughly constant astigmatism budget, but this is why it has the shape it does.

Sphere and cylinder trade off

M = S + C/2 above also means sphere and cylinder aren't independent: reducing cylinder and adding half that amount to sphere describes almost the same correction. A lens with less cylinder can substitute for a prescription with more, given a sphere shift. calcSphericalEquivalents in philscore.ts implements exactly this trade-off; see Matching Algorithm for the exact steps.

One more asymmetry

Under-correcting a far-sighted (positive sphere) prescription is worse than the reverse. smallerLensSphereScore in philscore.ts only penalizes that direction, leaving the symmetric case alone.