Mastery course · Software and Computational Perfumery

Fragrance Calculations in Python, Checked

Write a short Python program for pure active, formula, absolute and relative percentages and a recorded-limit flag, with tests against hand-worked values.

In short

The standard formula figures take a dozen lines of Python, and writing them is the best way to be sure you know their definitions. Pure material is weight × dilution for every non-solvent line; formula % is line weight over total weight; absolute % is pure over total weight; relative % is pure over total pure. A program earns trust only when it is tested against values you worked by hand and made to agree with an independent calculation.

What you will learn

  • Implement the standard formula figures from their definitions in a few lines of Python
  • Write assertions against hand-worked values so the program fails loudly when wrong
  • Reproduce an app's or spreadsheet's figures and explain any difference by definition or rounding
  • Know what the program does not check

Before this lesson: A Data Model for Formulas and Materials, Absolute and Relative Percentage: Study Guide, Working a Formula in the RUŌOD Lab App

Before you start

You need the definitions from absolute and relative percentages, the records from the data model lesson, and the habit of checking a tool from the Level 5 app workflow. Install Python 3.10 or later; nothing else is needed. This is an external exercise: the RUŌOD Lab app does not run scripts.

The program

from dataclasses import dataclass

@dataclass
class Line:
    material: str          # a stable material id, never the display name
    weight: float          # grams, as weighed
    dilution: float        # percent of pure material in what was weighed (100 = neat)
    solvent: bool = False  # True for a solvent line
    limit: float | None = None  # a relative-% limit YOU recorded, with its source

def r4(x):
    return round(x + 0.0, 4)

def calculate(lines):
    total = sum(l.weight for l in lines)
    pure = {l.material: (0.0 if l.solvent else l.weight * l.dilution / 100) for l in lines}
    total_pure = sum(pure.values())
    rows = []
    for l in lines:
        p = pure[l.material]
        rows.append({
            "material": l.material,
            "pure_g": r4(p),
            "carried_solvent_g": r4(l.weight - p),
            "formula_pct": r4(100 * l.weight / total),
            "absolute_pct": r4(100 * p / total),
            "relative_pct": r4(100 * p / total_pure) if total_pure else 0.0,
            "over_limit": l.limit is not None and total_pure > 0 and 100 * p / total_pure > l.limit,
        })
    return {"total_g": r4(total), "pure_g": r4(total_pure),
            "absolute_pct": r4(100 * total_pure / total), "rows": rows}

Values are rounded to four decimal places, the precision the RUŌOD Lab calculation engine reports, so the two can be compared digit for digit.

An example, with its tests

formula = [
    Line("bergamot-oil-fcf", 30.0, 100),
    Line("hedione", 25.0, 100),
    Line("iso-e-super", 20.0, 100),
    Line("ambroxan", 5.0, 10),        # weighed as a 10% dilution
    Line("galaxolide", 15.0, 50),     # supplied at 50%
    Line("ethanol", 5.0, 100, solvent=True),
]
result = calculate(formula)

assert result["total_g"] == 100.0
assert result["pure_g"] == 83.0             # 30 + 25 + 20 + 0.5 + 7.5
assert result["rows"][3]["pure_g"] == 0.5   # 5 g at 10%
assert result["rows"][4]["relative_pct"] == r4(100 * 7.5 / 83)
assert abs(sum(r["relative_pct"] for r in result["rows"]) - 100) < 0.001

Run, it prints total 100.0, pure 83.0 and absolute 83.0%, and relative percentages of 36.1446 (bergamot), 30.1205 (Hedione), 24.0964 (Iso E Super), 0.6024 (Ambroxan) and 9.0361 (Galaxolide). The figures are an arithmetic example, not a formula to make. Every assertion encodes a number worked by hand first; that is the point.

What the program does not do

  • It has no regulatory data. limit is whatever you recorded. A flag is not a compliance decision; constituents of naturals are not summed.
  • It trusts its inputs. A dilution entered as 100 for a 10% stock gives confident nonsense.
  • Duplicate ids collide. Two lines of one material overwrite each other in the dictionary. Either merge them deliberately or key by line number; the exercise asks you to fix this.

In professional practice

Formulation software is commonly validated by exactly this method: an independent calculation of a set of test formulas, compared figure by figure. The Lab blog's guide to testing perfume software describes the wider procedure.

Practical exercise: Make the program agree with two other calculations

About 150 minutes · You will have: a tested Python script for one of your formulas, its output, and a three-way comparison with a hand calculation and a spreadsheet or the app

  1. Type the program and run the example. Confirm the printed values.
  2. Fix the duplicate-material weakness: make two lines of the same material work, and add an assertion that proves it.
  3. Enter a real formula of yours with at least eight lines, two diluted and one solvent. Work three of its figures by hand first and assert them.
  4. Calculate the same formula in a spreadsheet or in the RUŌOD Lab app. Compare every figure.
  5. Explain each difference: rounding, solvent treatment, a dilution entered differently, or an error.

You have done this well if the assertions were written from hand calculations before the program ran, the duplicate-line fix has its own test, all three calculations agree to the fourth decimal or each difference is explained by definition, and you can say which figures the program cannot check.

Common mistakes

  • Writing the expected values from the program's own output. That tests nothing.
  • Exact equality on unrounded floats. Round or use a tolerance.
  • Counting solvent as pure material. It changes every relative percentage.

Key terms

Assertion
A line of code that stops the program if a stated condition is false. Here it encodes a value you worked out by hand.
Floating-point rounding
The small binary errors in decimal arithmetic on computers, which make 0.1 + 0.2 slightly different from 0.3. Compare with a tolerance, or round before comparing.
Treat as solvent
A flag that marks a line as solvent: it adds weight to the formula but no pure fragrance material.

Knowledge check

1. In the example, Galaxolide is 15 g of a 50% material in 100 g of formula. What are its absolute and relative percentages?
Show the answer

7.5% absolute and about 9.04% relative. Pure is 7.5 g; absolute is 7.5/100; relative is 7.5/83, the total pure material.

2. Why compare floating-point results with a tolerance or after rounding?
Show the answer

Binary arithmetic makes some exact decimal results differ in the last digits. A test that demands exact equality of unrounded floats can fail on a correct program.

3. Your script and an app disagree on relative % for one line. What is the first thing to check?
Show the answer

Whether both treat the solvent and the line's dilution the same way. Most disagreements are definitions: what counts as pure material, and what is solvent.

Written and reviewed by the RUŌOD Lab team. This is general education, not legal, regulatory, medical or safety advice. Example dilutions and exercises are for learning, are not validated products, and have not been safety-assessed. Completing a lesson confers no qualification. How we check these lessons.