Skip to content

Glasses Matching Algorithm

This document provides an explanation of the glasses matching algorithm. The algorithm's purpose is to filter and score glasses based on a given prescription.

The corresponding code can be found here.

Current Status

The algorithm is a replica of the one used in the previous REIMS1 system. We have comprehensive testing in place to ensure that the REIMS2 implementation aligns with the REIMS1 implementation. Any discrepancies are treated as bugs and are fixed accordingly.

We are open to refining the algorithm, see Potential Improvements for more information.

Glossary

  • lens: Refers to either OS or OD of an existing pair of glasses in the storage.
  • rx: The desired prescription.
  • delta: The absolute difference between a value of the desired prescription and the lens.
  • PhilScore: Name of the resulting score. The lower the PhilScore, the better the match. A PhilScore of 0 is a perfect match. The PhilScore is usually between 0 and 3 for most searches.

Algorithm Overview

The glasses matching process is executed in the following sequence:

Initially, we exclude glasses based on the following criteria:

  1. Filter glasses by type (single- or multifocal).
  2. Exclude glasses with incorrect axis using checkForAxisTolerance. This is performed once for OD and once for OS.
  3. Exclude glasses by tolerance of sphere + cylinder using checkForTolerances. This is performed once for OD and once for OS.
  4. If multifocal, exclude glasses where the lens' additional deviates from the desired additional more than tolerated. This is performed once for OD and once for OS.

Note on BAL

If the rx has BAL enabled for an eye, we bypass steps 2 and 4 for that eye. Step 3 for that eye is executed using the sphere and cylinder of the non-BAL eye with an increased tolerance of 1.0.

After filtering, we calculate the PhilScore for the remaining glasses using the following steps:

  1. Calculate the initial PhilScore for OD and OS separately using calcInitialDiffScore.
  2. Adjust the initial PhilScore for OD and OS separately based on the rules in calcSingleEyePhilscore.
  3. Sum the OD and OS philscore to get the final PhilScore.
  4. Arrange the glasses by PhilScore in ascending order.

Note on BAL

If the rx has BAL enabled for an eye, we calculate the Philscore only for the non-BAL eye, and the resulting PhilScore for the BAL eye is always set to 0.

Filtering Function Descriptions

checkForTolerances

This function verifies if the sphere and cylinder of a lens are within the desired tolerance of the prescription. It compares each glass to the desired prescription sphere and cylinder, taking into account the tolerance.

It also checks against every spherical equivalent of the prescription. If the lens is within the tolerance of any spherical equivalent, it is also considered.

NOTE

The default tolerance is 0.5. If the user specified "high tolerance" in the UI, the tolerance is increased to 1.0.

checkForAxisTolerance

This function is invoked for every single lens. It calculates the absolute axis tolerance based on the lens (❗ crucially, not rx ❗) cylinder. A higher lens cylinder allows for less axis tolerance. It calculates the allowed range, that is, rx axis plus or minus tolerance (and accounts for wraparound at 180 degrees). It checks whether the lens axis is inside the range.

Details: Axis tolerance mapping table
CylinderAxis tolerance
Less than -37
-3 to -2.258
-29
-1.75 and -1.510
-1.2513
-115
-0.7520
-0.525
-0.2535
090

Example for checkForAxisTolerance

Let's consider the following inputs:

  • Available lens cylinder: -1.0
  • Available lens axis: 178
  • Desired prescription (rx) axis: 10

Here's how it works with these inputs:

  1. Using the mapping table, we know that a cylinder of -1.0 has an axis tolerance of ±15 degrees.
  2. With a desired prescription axis of 10, the initially calculated allowed range is -5 to 25 degrees.
  3. However, an axis of -5 degrees doesn't exist. So, the function adjusts the allowed range to 175 to 180 degrees (for the negative part) and 0 to 25 degrees (for the positive part).
  4. The function then checks if the lens axis (178 degrees) falls within this allowed range. In this case, the lens is allowed because 178 is between 175 and 180.

Scoring Function Descriptions

calcInitialDiffScore

This function is invoked for every single lens (i.e., for each eye of every glass that hasn't been filtered before).

It calculates the initial PhilScore based on the sum of the deltas of sphere, cylinder, axis (and additional). The deltas are weighted, i.e., the sphere and cylinder deltas count a lot more than the axis and additional deltas.

ParameterWeight
Sphere delta1
Cylinder delta1
Additional delta0.1
Axis delta1/3600

Notice

This has the consequence that a high difference of axes is basically irrelevant for the score, and a difference in additionals only has a small impact. See Potential Improvements for more information.

The function then returns the initial PhilScore for the lens.

Example for calcInitialDiffScore

Given the following inputs:

  • Desired Prescription (rx)

    SphereCylinderAdditionalAxis
    +2.0-1.0+1.590
  • Available Lens

    SphereCylinderAdditionalAxis
    +2.5-0.75+1.2585

The function calcInitialDiffScore calculates the deltas, weights them, and sums them up:

  • Weighted sphere delta: |2.5 - 2.0| * 1 = 0.5
  • Weighted cylinder delta: |-0.75 - (-1.0)| * 1 = 0.25
  • Weighted additional delta: |1.25 - 1.5| * 0.1 = 0.025
  • Weighted axis delta: |85 - 90|/3600 = 0.00139

The initial PhilScore is the sum of these: 0.5 + 0.25 + 0.025 + 0.00139 = 0.77639.

NOTE

Without the axis and additional deltas, the PhilScore would be 0.75. So it does have an impact, but it's close to negligible.

calcSingleEyePhilscore

The score is then adjusted based on several conditions related to the sphere, cylinder, and additional values. The conditions are applied in the following order:

Improving the score means subtracting from it so it gets smaller. Worsening it means adding to it.

  1. Improve the score (by 0.5) if the lens is a spherical equivalent of the prescription (I'm actually not sure if this is what's happening, please fact check this using the code in the details section).

    Additional details
    • This is the exact condition:
    ts
    if ((rxSphere - lensSphere)  === ((lensCylinder - rxCylinder) / 2) AND
     rxSphere > lensSphere AND
     cylinderDiff <= 1
    ) // NOTE: cylinder is always negative
    • The score gets improved slightly more (by an additional amount of 0.05) if the lens sphere is bigger than 0
  2. Only if step 2 did not apply: Improve the score if lens sphere is larger than desired sphere AND lens cylinder is smaller than the desired cylinder. OR the other way round (sphere smaller AND cylinder larger).

    Additional details
    • This is the exact condition:
    ts
      if (lensSphere > rxSphere AND rxCylinder > lensCylinder)
         OR
         (lensSphere < rxSphere AND rxCylinder < lensCylinder)
    • The score gets improved by 0.5 if the cylinder delta is larger than 0.25, otherwise by 0.25
    • The score gets improved slightly more (by an additional amount of 0.05) if sphere delta matches cylinder delta
  3. Only if step 2 or 3 did not apply: Improve the score (by 0.12) if the spheres are equal and the cylinder delta is small (<= 0.75).

  4. Improve the score by a really small amount if the search is multifocal and the lens additional is larger than the desired additional.

    Additional details
    • Score gets improved more if the difference is larger. Formula for how the score is improved: -(lensAdd - rxAdd) / 100
    • This also has basically no impact, since the maximum delta with the normal tolerance would be 0.25, so the highest score improvement would be: -0.25/100 = -0.0025
  5. Worsen the score (by 0.25) if the desired sphere is positive AND if it's larger than the lens sphere.

The function then returns the final PhilScore for the lens.

Potential Improvements

Some ideas for potential improvements are listed below. These are not exhaustive and are open to discussion.

For calcInitialDiffScore

The weights for the additional delta and axis in the initial PhilScore could be adjusted so they actually have an impact. Currently, they are basically ignored.

This would have the potential benefit that the filtering by axis could be less strict, as the axis would then have a larger impact on the score. This was a common cause for complaints of the matching process.

For calcSingleEyePhilscore

  1. The conditions in calcSingleEyePhilscore could be evaluated independently, rather than skipping some if others apply.
  2. The rationale for improving the score more in condition 2 if the cylinder delta is larger is unclear.
  3. The rationale for improving the score more in condition 4 if the difference is larger is unclear.

In General

Converting the PhilScore to a percentage or a more intuitive scale could be beneficial. This would make it easier to understand the quality of the match without prior knowledge.

For example: a PhilScore of 0 could be a 100% match, and a score of 4 could be 0 %, with linear interpolation in between (so a score of 2 would be 50%). We can also do a more complex mapping than just linear, but this could be a good start.