Skip to content

quantities Module — Specification

Purpose

The quantities module is the first LCA domain module in chwr_LCA. It converts a project's Bill of Quantities (BoQ / devis quantitatif) — a list of construction work items expressed in whatever unit a quantity surveyor naturally measures them in (m³, m², linear metres, unit count, lump sum) — into the raw material quantities that every downstream LCA module (impacts, costing, etc.) consumes: volumes and masses of rock, gravel, sand, earth, water, cement, steel, glass, bitumen, paints, oils, tiles, etc.

This document reverse-engineers that conversion logic from a single reference workbook built by the project's domain expert(s) for a real building (a school complex in Senegal). It is written so that a Claude Code session can implement the module and pass a 0.1%-tolerance regression test without opening the spreadsheet.

Nothing here should be read as validated science. The workbook encodes a practitioner's empirical rules of thumb for Sahelian earth-construction and conventional reinforced-concrete/masonry construction. Every constant is reported as found; several are demonstrably inconsistent with each other or with their own documentation. These are reported, not corrected — see Errors & inconsistencies found.


Source workbook

Filename 1a_boq_input_lca.xlsx
Sheets 1 — Quantitatifs (French: "quantities/take-offs")
Used data range A1:X77 (the sheet's nominal used-range extends to AW995, but everything past X77 is empty except one orphaned cell comment at AW43 — see E17)
Project instance in the sheet "Keur Songho School Complex" (cell A1, merged A1:B1) — a real reference building used as the worked example throughout this spec
Date analysed 2026-07-13
Named ranges none
Data validation rules none (no dropdowns constrain the Unit or N° columns — see E15)
Sheet protection none

Input contract

The sheet has exactly one data table, one row per BoQ line item, columns AE are input/description, columns GX are computed output (material quantities). Column F is an always-empty spacer column.

Col Header (row 2) Type Description
A string Hierarchical devis code. Two kinds of row share this column: section headers (e.g. A, III, V) and line items (e.g. A.III.1, B.II.1.a). See "Row classification" below.
B Description of Works string (French devis phrasing, already translated to English in this copy) Free-text description of the work item, frequently containing the geometric assumptions (course counts, wall heights, dimensions) used to derive the per-unit material ratios in column E.
C U (Unité) enum-like string One of , , ml (mètre linéaire), u/ud (unité — piece count), ff (forfait — lump sum). Not validated: row 27 contains a data-entry error where the full description was pasted into this cell instead of a unit code (E15), and four cells (C7,C9,C11,C12) carry a trailing space ('m³ ') (E21). A Pydantic UnitEnum parser for this column must .strip() and must handle the corrupted case defensively (reject, don't guess).
D Quantity float, > 0 This is the actual user input. The measured quantity of the work item, in the unit stated in column C. This is the only column a user is expected to type into for line-item rows; every other output column is derived from it. Two cells (D5,D6) are themselves formulas (=82.412*0.5/0.95, =82.412*0.45/0.95) that split one external take-off total across two calculation sub-rows — see "Row 5/6" below and E1. All other D cells are plain literals. Several D values (D41/D72; D42/D35; D44/D70; D53/D57) are identical floats reused across two different line items (e.g. the same wall area receives two coats of finish) — see E9's discussion of non-referential duplication.
E Conversion free text, not a formula A human-readable transcription of the ratio(s) used to derive the material columns from D. This is documentation, not a computed cell — it can and does drift from the actual formula in GX (see Errors section). Treat it as the stated intent; treat the GX formulas as ground truth for parity.
F (none) Always empty. Visual spacer between inputs and outputs.
G–X Material columns (see Unit conventions) float, formula Computed, not input. These are the module's output surface.
row 77 sum formula (SUM) Project-wide total per material column. This is the module's top-level output: total material quantities for the whole BoQ.

Row classification (header vs. line item)

There is no explicit "row type" flag, but the pattern is 100% consistent and can be used directly as the parser rule:

  • Section header row: column D is empty, column A bold, with either a light-blue fill (top-level division, e.g. A = "Structural work", B = "Finishes") or a grey fill (section, e.g. III = "FONDATIONS") or no fill but still bold (sub-section, e.g. II.1-style groupings like row 49 "Slabs"). These rows carry no quantity and no material output; they exist purely to group line items for the human reader and to build the hierarchical prefix used by line items below them.
  • Line item row: column D is populated (formula or literal), column A not bold, no fill. This is a real BoQ line that produces material quantities.

Rule for implementation: is_header_row := (D is None). This is simpler and more robust than parsing the format (which mixes letters, Roman numerals, and dotted codes) and doesn't depend on cell styling being preserved through any future re-export.

N° code structure

{Division}.{Section}.{Item}[.{sub-item}], e.g. B.II.3.a: - Division: A (Structural work) or B (Finishes) — single letter. - Section: Roman numeral (III, IV, V, …) — restarts at III for division A (there is no I/II in this instance; presumably sections I–II are sitework/earthworks not present in this particular BoQ) and restarts at I for division B. - Item: integer. - Sub-item: optional lowercase letter, used when a section (e.g. "Slabs") groups several distinct sub-works (e.g. "Compacted earth fill" .a, "Reinforced concrete slab" .b).

Two irregularities in this instance, both real, both to preserve rather than silently fix: - A.III.1 is deliberately reused across two consecutive rows (5 and 6) — this is the one legitimate case of code reuse: a single composite BoQ line ("mixed foundation... 50cm cyclopean earth concrete... 45cm cyclopean concrete...") whose two layers are calculated on separate rows because they have different material recipes. A parser must be able to group by and sum, or treat every physical row as an independent calculation line and aggregate at the end — the sheet does the latter (the sum row sums all rows regardless of shared codes), so that is the safe behaviour to replicate. - A.VI.4 is reused across rows 24 and 25 for two different work items ("90 cm arches on 38 cm thick walls" and "40 cm arches on 38 cm thick walls") — this is a genuine duplicate-code data-entry error, not a composite item. See E16.

Summary of the contract a user must satisfy per line item

  1. — a devis code (free text in this source; not validated).
  2. Description of Works — free text.
  3. U — one of {m³, m², ml, u, ff} (must be validated on ingestion; the source file itself fails this validation once — see E15).
  4. Quantity — positive float in the stated unit. No explicit range checks exist in the source; no line item in this instance is zero or negative.

Everything else (columns G–X, and the sum row) is derived, not part of the input contract, and is exactly what this module must reproduce.


Sheet-by-sheet logic

Only one sheet exists, so there is no cross-sheet dependency graph. Internally, the dependency structure is:

D (line quantity, user input, occasionally itself a formula off an external constant)
  ├─▶ per-line material formulas in G..X (each cell depends only on its own row's D,
  │    except for the anomalies below)
  └─▶ row 77 SUM(col3:col76) per material column — the project totals

Anomalous (row-crossing) dependencies — all flagged in Errors, listed here because they change the dependency graph itself:

  • K16 depends on D15, not D16 (wrong-row reference bug, E3).
  • K29 depends on J29 (the row's own computed Earth quantity), not directly on D29 — this is the documented method (water is a fraction of earth volume, not of the raw linear-metre input).
  • K30 depends on D30 directly, not on J30 — breaking the pattern K29 establishes one row above it for what the sheet's own comment says is the same method (E8).
  • J29 and K29's chain effectively hardcodes the line's own quantity (18.6) instead of referencing D29 (E7) — numerically inert here only because D29 == 18.6.
  • U36 hardcodes a rounded copy of D35 (289.15 instead of 289.152/=D35) rather than referencing it (E9).
  • Row 77 (sum) depends on the full column range row3:row76 for every material column — this is the only genuinely cross-row, "whole sheet" formula, and it is correct and consistent for all 18 material columns.

There is no separate "materials master" sheet, "factors" sheet, or "assemblies" sheet. Every conversion factor is inlined directly into the formula of the cell that uses it. This has a direct design implication for the Python port, raised in Open Questions: the source of truth for "how much sand does 1 m³ of 300 kg/m³ concrete need" is duplicated verbatim wherever that concrete mix is used (rows 9, 11, 12 all repeat *0.53 for sand at 350 kg/m³ dosage; rows 51 repeats a different ratio for 300 kg/m³) rather than being centralised. The Python implementation should almost certainly centralise these into a lookup keyed by (material recipe, dosage) rather than reproducing the copy-paste structure — but the values transcribed below are authoritative regardless of how they get organised in code.


Formulas (transcribed)

Below is every formula-bearing line item, grouped by division/section exactly as laid out in the sheet. D is the line's own quantity input. Formulas are transcribed exactly as they appear in the workbook, including the bugs — each bug is tagged ⚠Ex and cross-referenced to Errors & inconsistencies found. Do not "fix" a tagged formula when porting it; the parity fixture is built against the buggy output.

Units for each output column are given once, in Unit conventions, below — they are not repeated per line here.

A — Structural work

III. FONDATIONS (Foundations)

A.III.1 (row 5) — "Mixed foundation: 50 cm of cyclopean earth concrete (80% gravel – 20% soil) / 45 cm of cyclopean concrete (80% gravel – 20% concrete, dosed at 250 kg/m³)" — unit , split across two rows.

  • D5 = 82.412 * 0.5 / 0.9543.37473684. 82.412 is an external total (not derived anywhere in this sheet — E1, PROVENANCE UNKNOWN); 0.5/0.95 and 0.45/0.95 (row 6) allocate that total proportionally to each layer's thickness (50 cm and 45 cm, which sum to 0.95 m — the denominator is that sum, not a waste factor).
  • Rock (G5) = D5 * 0.84
  • Earth (J5) = D5 * 1.5 * 0.21
  • Water (K5) = D5 * (0.01 + (0.45 * 0.21))

A.III.1 (row 6) — second layer of the same composite item, unit .

  • D6 = 82.412 * 0.45 / 0.9539.03726316.
  • Rock (G6) = D6 * 0.84
  • Gravel (H6) = D6 * 0.53 * 0.21
  • Sand (I6) = D6 * 0.75 * 0.21
  • Water (K6) = D6 * (0.01 + (0.08 * 0.21))
  • Cement (L6) = D6 * 250 * 0.21
  • ⚠E2: description states the concrete/soil fraction is 20%; every formula in rows 5 and 6 multiplies by 0.21, not 0.20. Unexplained 1-point discrepancy, present consistently in both rows (so probably deliberate, but undocumented).

A.III.2 (row 7) — "Clean (blinding) concrete, 5 cm thick, dosed at 150 kg/m³" — unit (trailing space, E21), D7 = 0.576.

  • Gravel (H7) = D7 * 0.56
  • Sand (I7) = D7 * 0.38
  • Water (K7) = D7 * 0.08
  • Cement (L7) = D7 * 150

A.III.3 (row 8) — "Capillary barrier – polyane film" — unit ml, D8 = 139.6.

  • Polyethylene film (P8) = D8 * 1 — treats a 1 m roll depth as constant, converting linear metres directly to m² of film 1:1.

A.III.4 (row 9) — "Reinforced concrete for isolated footing, dosed at 350 kg/m³ (1.2 x 1.2 x 0.4 m)" — unit (trailing space), D9 = 4.608.

  • Gravel (H9) = D9 * 0.67
  • Sand (I9) = D9 * 0.53
  • Water (K9) = D9 * (0.01 + 0.18)
  • Cement (L9) = D9 * 350
  • Steel Rebars (M9) = D9 * 100

This is the canonical "350 kg/m³ reinforced concrete" recipe: 0.67 m³ gravel, 0.53 m³ sand, 0.19 m³ water, 350 kg cement, 100 kg rebar per m³. It recurs verbatim in rows 11 and 12.

IV. Reinforced concrete in elevation

A.IV.1 (row 11) — "Reinforced concrete for column, dosed at 350 kg/m³" — unit , D11 = 1.208. Same recipe as row 9: H11=D11*0.67, I11=D11*0.53, K11=D11*(0.01+0.18), L11=D11*350, M11=D11*100.

A.IV.2 (row 12) — "Reinforced concrete for beam, dosed at 350 kg/m³ (beam base at 2.5 m)" — unit , D12 = 7.49. Same recipe: H12=D12*0.67, I12=D12*0.53, K12=D12*(0.01+0.18), L12=D12*350, M12=D12*100.

V. Walls (adobe)

All wall rows follow the pattern: Earth = D × [(bricks/m² or bricks/unit) × 0.0143 m³ earth/brick + (joint fraction) × 1.5 m³ earth/m³ joint], Water = D × [(bricks) × 0.0043 m³ water/brick + (joint fraction) × 0.45 m³ water/m³ joint]. The per-brick and joint constants (0.0143, 0.0043, 1.5, 0.45) are consistent across every wall/arch/vault row in the sheet — see Factors table. The brick-count and joint-fraction numbers themselves are geometry take-offs specific to this building's wall details (thickness, course count), not general factors — see Open Questions on whether these belong in code as per-assembly-type constants.

A.V.1 (row 14) — "Walls made of adobe bricks, 80 cm thick (Height 2.52 m, 14 courses)" — unit , D14 = 70.308. - Earth (J14) = D14 * ((55.56 * 0.0143) + (0.3 * 1.5)) - Water (K14) = D14 * ((55.56 * 0.0043) + (0.3 * 0.45))

A.V.2 (row 15) — "…80 cm thick, with BBSC on header course (Height 2.16 m, 12 courses)" — unit , D15 = 40.176. - Rock (G15) = D15 * (20.83 * 0.0002) — BBSC bricks contribute only rock in this model (see Open Questions / new-category discussion). - Earth (J15) = D15 * (41.66 * 0.0143 + 0.21 * 1.5) - Water (K15) = D15 * ((41.66 * 0.0043) + (0.21 * 0.45))

A.V.3 (row 16) — "…65 cm thick (Height 2.52 m, 14 courses)" — unit , D16 = 19.152. - Earth (J16) = D16 * ((41.67 * 0.0143) + (0.21 * 1.5)) - Water (K16) = D15 * ((41.67 * 0.0043) + (0.21 * 0.45)) — ⚠E3: references D15 (40.176, the previous row's 80 cm-wall quantity), not D16 (19.152, this row's own 65 cm-wall quantity). Reproduce as written for parity.

A.V.4 (row 17) — "…38 cm thick, with BBSC on header course (Height 4.32 m, 23 courses)" — unit , D17 = 238.464. - Rock (G17) = D17 * (27.78 * 0.0002) - Earth (J17) = D17 * ((27.78 * 0.0143) + (0.12 * 1.5)) - Water (K17) = D17 * ((27.78 * 0.0043) + (0.12 * 0.45))

A.V.5 (row 18) — "…38 cm thick (Height 4.32 m, 23 courses)" — unit , D18 = 74.52. - Earth (J18) = D18 * ((27.78 * 0.0143) + (0.12 * 1.5)) - Water (K18) = D18 * ((27.78 * 0.0043) + (0.12 * 0.45))

A.V.6 (row 19) — "…20 cm thick (Height 3.89 m, 21 courses)" — unit , D19 = 37.719. - Earth (J19) = D19 * ((13.89 * 0.0143) + (0.04 * 1.5)) - Water (K19) = D19 * ((13.89 * 0.0043) + (0.04 * 0.45))

VI. Arches capping the openings

A.VI.1 (row 21) — "90 cm arches on 80 cm thick walls" — unit u, D21 = 13. - Earth (J21) = D21 * ((24.74 * 0.0143) + (0.08 * 1.5)) - Water (K21) = D21 * ((24.74 * 0.0043) + (0.08 * 0.45))

A.VI.2 (row 22) — "90 cm arches on 65 cm thick walls" — unit u, D22 = 4. - Earth (J22) = D22 * ((19.79 * 0.0143) + (0.06 * 1.5)) - Water (K22) = D22 * ((19.79 * 0.0043) + (0.06 * 0.45))

A.VI.3 (row 23) — "1.2 m arches on 38 cm thick walls" — unit u, D23 = 2. - Gravel (H23) = D23 * 0.05 - Earth (J23) = D23 * ((13.19 * 0.0143) + (0.03 * 1.5)) - Water (K23) = D23 * ((13.19 * 0.0043) + (0.03 * 0.45))

A.VI.4 (row 24) — "90 cm arches on 38 cm thick walls" — unit u, D24 = 12. - Earth (J24) = D24 * (9.9 * 0.0143) + (0.03 * 1.5) — ⚠E4: missing outer parentheses. The joint term (0.03 * 1.5) = 0.045 is added as a flat constant, not scaled by D24, unlike every sibling wall/arch row. As written this evaluates to 1.74384; the pattern used everywhere else would give 2.23884. - Water (K24) = D24 * (9.9 * 0.0043) + (0.03 * 0.45) — ⚠E4, same bug. As written: 0.52434; pattern-consistent value would be 0.67284.

A.VI.4 (row 25) — "40 cm arches on 38 cm thick walls" — unit u, D25 = 2. ⚠E16: same code (A.VI.4) as row 24 for a genuinely different work item — duplicate code, not a composite split. - Earth (J25) = D25 * ((4.4 * 0.0143) + (0.01 * 1.5)) — correctly parenthesised. - Water (K25) = D25 * ((4.4 * 0.0043) + (0.01 * 0.45)) — correctly parenthesised.

A.VI.5 (row 26) — "90 cm arches on 20 cm thick walls" — unit u, D26 = 3. - Earth (J26) = D26 * ((9.9 * 0.0143) + (0.03 * 1.5)) - Water (K26) = D26 * ((9.9 * 0.0043) + (0.03 * 0.45))

A.VI.6 (row 27) — "Large openings on 38 cm thick walls. Width of 1.8 m, installation of the arch at 2.52 m (14th course)" — unit column corrupted (⚠E15: cell C27 contains the full description text instead of a unit code; sibling rows and the row's own E-note ("ud *…") indicate the intended unit is u), D27 = 6. - Gravel (H27) = D27 * 0.01 - Earth (J27) = D27 * ((22 * 0.0143) + (0.06 * 1.5)) - Water (K27) = D27 * ((22 * 0.0043) + (0.06 * 0.45))

VII. Nubian vaults

A.VII.1 (row 29) — "Vaults with a 3.3 m span, loaded over the height h of the vault" — unit ml, D29 = 18.6. - Earth (J29) = 18.6 * (3.14 * 3.3) * 0.25 — ⚠E7: hardcodes the line's own quantity (18.6) instead of =D29*(3.14*3.3)*0.25. Numerically inert here (D29 == 18.6) but brittle — if D29 is edited, J29 will silently go stale. Semantics: linear-metre quantity × semicircle arc length for a 3.3 m-radius vault (π·r) × 0.25 m brick thickness (the sheet's own note mislabels this "0.25cm" — physically the arithmetic only works if it means 0.25 m; see E19). - Water (K29) = J29 * 0.2585 — water as a fraction of the computed earth volume, not of the raw D. 0.2585 derives from the note's own worked example: 0.0043 m³ water ÷ (0.42 × 0.22 × 0.18) m³ brick bounding-box = 0.2585. This is a different water:earth ratio basis than the 30% ratio used in wall rows (0.0043÷0.0143 = 30.07%, based on net brick earth-content, not the brick's gross bounding box) — see Factors table for both, they are not interchangeable.

A.VII.2 (row 30) — "Vaults with a 2.6 m span, loaded over the height h of the vault" — unit ml, D30 = 53.4. - Earth (J30) = D30 * (2.6 * 3.14) * 0.25 — correctly referenced this time. - Water (K30) = D30 * 0.2585 — ⚠E8: multiplies the raw linear-metre input D30 by 0.2585, not the row's own computed earth volume J30, breaking the method the sheet itself documents one row above (and repeats in this row's own E-note: "Water is = 0.0043/(0.420.220.18) m3 per m3 earth"). As written: 13.8039. If the documented method (J30 * 0.2585) were applied instead: 28.17376, more than double. This is the single largest-magnitude formula inconsistency found in the workbook and should be treated as high-priority for Marco to resolve (see Open Questions) — reproduce the as-written value for parity, but do not propagate this pattern to new code.

VIII. Loading of the vaults

A.VIII.1 (row 32) — "Lateral buttresses on 80 cm thick walls with BBSC on header course for 3.3 m vault" — unit ml, D32 = 18.6. - Rock (G32) = D32 * (30 * 0.0002) - Earth (J32) = D32 * ((66.9 * 0.0143) + (0.23 * 1.5)) - Water (K32) = D32 * ((66.9 * 0.043) + (0.23 * 0.45)) — ⚠E6: coefficient 0.043, not the documented 0.0043 (a factor-of-10 decimal-point slip). As written: 55.43172 m³; with the documented 0.0043 coefficient: 7.275762 m³ — a 7.6× overstatement of water for this single line, the largest unit-value error found. Reproduce as written for parity; flag strongly to Marco.

A.VIII.2 (row 33) — "Central buttresses on 80 cm thick walls for 2.6 m vault" — unit ml, D33 = 62.3. - Earth (J33) = D33 * ((114.95 * 0.0143) + (0.7 * 1.5)) - Water (K33) = D33 * ((114.95 * 0.0043) + (0.7 * 0.45)) — correct coefficient here.

IX. Waterproofing

A.IX.1 (row 35) — "Slope and waterproofing on the roof" — unit , D35 = 289.152. - Earth (J35) = D35 * ((8.13 * 0.0143) + (0.04 * 1.5)) - Water (K35) = D35 * ((8.13 * 0.0043) + (0.04 * 0.45))

A.IX.2 (row 36) — "Waterproofing and anti-termite treatment in the foundation" — unit ff (lump sum), D36 = 1. - Anti-termite (U36) = D36 * 5 * 289.15 — ⚠E9: 289.15 is a manually re-typed, rounded copy of D35 (289.152), not a cell reference. Numerically the drift is negligible (well under the 0.1% parity tolerance) but the pattern — hardcoding a copy of another row's value instead of referencing it — is a latent bug: if D35 changes, U36 will not follow. As written: 1445.75 L (5 L per m² of roof surface).

X. ACROTÈRE (parapet upstand wall)

A.X.1 (row 38) — "Acroterion low wall made of adobe bricks with BBSC on the exterior sides (Height 18 cm, 1 course)" — unit , D38 = 13.9824. - Rock (G38) = D38 * (13.89 * 0.0002) - Earth (J38) = D38 * (13.89 * 0.0143) + (0.04 * 1.5) — ⚠E5: same missing-parentheses bug as E4. As written: 2.837282165; pattern-consistent value: 3.616226. - Water (K38) = D38 * ((13.89 * 0.0043) + (0.04 * 0.45)) — correctly parenthesised (inconsistent with its own row's J38 — the bug is cell-local, not row-wide).

B — Finishes

I. Coatings

B.I.1 (row 41) — "Exterior cement plaster on walls" — unit , D41 = 344.4213566576118 (identical value to D72, row 72 — see note above on reused literals). - Sand (I41) = D41 * 0.016 * 0.88 - Water (K41) = D41 * 0.016 * 0.2 - Cement (L41) = D41 * 0.016 * 400

This is the canonical "16 mm cement plaster" recipe: per m² of wall, 0.016 m³ of plaster mix at 400 kg cement/m³, 0.88 m³ sand/m³ plaster, 0.2 m³ water/m³ plaster. It recurs (with the omission bug E13) in rows 43, 45, 46, 47.

B.I.2 (row 42) — "Exterior coatings of earth-sand-gravel-tar on roofs" — unit , D42 = 289.152 (= D35). - Sand (I42) = D42 * 0.042 * 0.32 - Earth (J42) = D42 * 0.042 * 0.11 - Water (K42) = D42 * 0.042 * 0.3 - Bitumen (Q42) = D42 * 0.042 * 31 - Crude oil (R42) = D42 * 0.042 * 5.5 — the E-note calls this "Petrol", the column header calls it "Crude oil" — see vocabulary note. - Waste Oil (S42) = D42 * 0.042 * 11 - ⚠E14: the line's own description names "gravel" as a mix constituent ("earth-sand-gravel-tar"), but neither the E-note nor any formula computes a Gravel quantity for this row. Gravel is silently dropped between description and calculation.

B.I.3 (row 43) — "Interior cement plaster on walls" — unit , D43 = 495.7002701443385. Same 16 mm-plaster recipe as row 41: I43=D43*0.016*0.88, K43=D43*0.016*0.2, L43=D43*(0.016)*400. ⚠E17: an orphaned cell comment (anchored at AW43, far outside the used data range, but semantically attached to this row) reads verbatim: "A vérifier avec Adama" ("to be checked with Adama"), dated 2024‑02‑08, author "USER". It was never resolved before the file was handed over. Flag to Marco — likely concerns either the 495.7 m² area figure or the plaster ratios for this specific row.

B.I.4 (row 44) — "Interior earth-sand plaster on vaults" — unit , D44 = 315.6358139061665 (= D70). - Sand (I44) = D44 * 0.016 * 0.84 - Earth (J44) = D44 * 0.016 * 0.24 - Water (K44) = D44 * 0.016 * 0.3

B.I.5 (row 45) — "Plastering on columns" — unit , D45 = 12.064. - Sand (I45) = D45 * 0.016 * 0.88 - Water (K45) = D45 * 0.016 * 0.2 - ⚠E13: the row's own E-note states "Cement = m20.016m3 plaster 400kg" identically to row 41, but no cement formula exists for this row. Cement demand is silently omitted for this line.

B.I.6 (row 46) — "Plastering on beams" — unit , D46 = 29.4. Same as row 45: I46=D46*0.016*0.88, K46=D46*0.016*0.2. ⚠E13 applies here too — no cement formula.

B.I.7 (row 47) — "Plaster for window reveals" — unit , D47 = 21. Same as row 45: I47=D47*0.016*0.88, K47=D47*0.016*0.2. ⚠E13 applies here too — no cement formula.

II. Slab and concrete works

B.II.1.a (row 50) — "Compacted earth fill (thickness 15 cm)" — unit , D50 = 32.553. - Earth (J50) = D50 * 1.5 - Water (K50) = D50 * 0.45

B.II.1.b (row 51) — "Reinforced concrete slab with a mix of 300 kg/m³ (thickness: 10 cm)" — unit , D51 = 21.702. - Gravel (H51) = D51 * 0.72 - Sand (I51) = D51 * 0.54 - Water (K51) = D51 * 0.15 - Cement (L51) = D51 * 300

This is the "300 kg/m³ concrete" recipe: 0.72 m³ gravel, 0.54 m³ sand, 0.15 m³ water, 300 kg cement per m³ — distinct from the 350 kg/m³ recipe (rows 9/11/12) and the 250 kg/m³ recipe (rows 6, 53, 55) — three different concrete mix designs, each with its own sand/gravel/water ratios, all hand-copied wherever used.

B.II.2.a (row 53) — "Concrete slabs (tiles) on parapet walls" — unit ml, D53 = 77.68. Cross-section stated as 0.35 m × 0.04 m, 250 kg/m³ dosage. - Gravel (H53) = D53 * 0.45 * 0.04 * 0.75 — ⚠E10: uses cross-section width 0.45, not the 0.35 used by every other coefficient in this same row (and stated in the description). As written: 1.04868 m³; with the row-consistent 0.35: 0.81564 m³ — a 28.6% overstatement of gravel for this line. - Sand (I53) = D53 * 0.35 * 0.04 * 0.53 — correct width. - Water (K53) = D53 * 0.35 * 0.04 * 0.13 — correct width. - Cement (L53) = D53 * 0.35 * 0.04 * 250 — correct width.

B.II.3.a (row 55) — "Window sills, 120 cm long / 8 cm thick" — unit , D55 = 0.863104. 250 kg/m³ dosage: - Gravel (H55) = D55 * 0.75 - Sand (I55) = D55 * 0.53 - Water (K55) = D55 * 0.13 - Cement (L55) = D55 * 250

B.II.4.a (row 57) — "Slope around the building" — unit ml, D57 = 77.68 (= D53). Cross-section triangle 1.5 m × 0.10 m. - Earth (J57) = D57 * ((1.5 * 0.1) * 1.5) - Water (K57) = D57 * ((1.5 * 0.1) * 0.45)

III. Metal and wooden carpentry works

Window/door recipe pattern: Steel plate volume = D × (leaf width × leaf height × 1.3 waste factor × 0.004 m sheet thickness); Steel profile volume = D × (frame perimeter × cross-sectional steel area of a 5×5×0.4 cm hollow profile [4 × 0.05 × 0.004 m²]); Glass volume (where a rounded fanlight exists) = half-circle area × 2 (panes) × 0.004 m thickness.

B.III.1.a (row 60) — "Louvered window with fixed rounded glazed part, 0.9 × 1.89 m" — unit u, D60 = 17. - Steel plates (N60) = D60 * 0.9 * 1.9 * 1.3 * 0.004 - Steel profiles (O60) = D60 * ((0.2*0.9) + (2*2*1.9)) * (4*0.05*0.004) — ⚠E11: the perimeter term uses 0.2*0.9, not the documented 2*0.9 (decimal-point slip on the leaf-width side of the perimeter). As written: 0.105808 m³; with 2*0.9: 0.12784 m³ — a 17.2% understatement. - Glass (T60) = 1/2 * 0.45 * 0.45 * 3.14 * 2 * 0.004 — hardcoded, not scaled by D60 at all: this cell computes the glass for one window regardless of the 17-unit quantity, then the sum row (T77) adds this single-window figure once. This is consistent with there being only one occurrence of a rounded-fanlight glass formula per distinct window type across the whole sheet (rows 60, 63, 64 each contribute exactly one glass line) — i.e. glass is being counted per distinct opening type, not per unit count D. This is very likely intentional (all the rounded-fanlight types in this project apparently have the same single physical opening needing this glass shape, or the modeler simply undercounted) but it is not stated anywhere in the sheet, so treat this as a real ambiguity — see Open Questions.

B.III.1.b (row 61) — "Louvered window, 0.4 × 0.74 m" — unit u, D61 = 2. - Steel plates (N61) = D61 * 0.4 * 0.74 * 1.3 * 0.004 - Steel profiles (O61) = D61 * ((2*0.4) + (2*0.74)) * (4*0.05*0.004) — correct, no double perimeter (single-leaf frame, different geometry convention from the doors below — this is consistent with its own description, not a bug).

B.III.2.a (row 63) — "Metal louvered door with fixed rounded glazed part, 1.2 × 3.12 m" — unit u, D63 = 2. Note: title says 3.12 m, but the E-note and every formula in this row consistently use 3.2 m, not 3.12 m (⚠E20 — description/formula mismatch of 8 cm; internally self-consistent, just disagrees with its own title). - Steel plates (N63) = D63 * 1.2 * 3.2 * 1.3 * 0.004 - Steel profiles (O63) = D63 * ((2*1.2) + (2*2*3.2)) * (4*0.05*0.004) - Glass (T63) = 1/2 * 0.6 * 0.6 * 3.14 * 2 * 0.004 — again not scaled by D63 (see B.III.1.a discussion).

B.III.2.b (row 64) — "Metal louvered door with fixed rounded glazed part, 90 × 279 cm" — unit u, D64 = 2. - Steel plates (N64) = D64 * 0.9 * 2.7 * 2.3 * 0.004 — ⚠E12: the E-note documents (0.9m * 2.79m surface * 1.3); the formula instead uses 2.7 (not 2.79) and 2.3 (not 1.3) — two separate slips compounding into a large error. As written: 0.044712 m³; with the documented 0.9 × 2.79 × 1.3: 0.026114 m³ — a 1.71× overstatement. - Steel profiles (O64) = D64 * ((2*0.9) + (2*2*2.79)) * (4*0.05*0.004) — uses the correct 2.79 here; the error is isolated to N64. - Glass (T64) = 1/2 * 0.45 * 0.45 * 3.14 * 2 * 0.004 — not scaled by D64 (same pattern as above).

B.III.2.c (row 65) — "Metal door (partition)" — unit u, D65 = 3, no glazing. - Steel plates (N65) = D65 * 1.2 * 3.2 * 1.3 * 0.004 - Steel profiles (O65) = D65 * ((2*1.2) + (2*2*3.2)) * (4*0.05*0.004)

B.III.3.a (row 67) — "Metal gutters (80 × 20 × 10 cm)" — unit u, D67 = 7. - Steel plates (N67) = D67 * ((2*0.1) + 0.2) * 0.8 * 0.004

IV. Paint

B.IV.1 (row 69) — "Painting on interior plaster (except vaults)" — unit , D69 = 558.1642701443385. - Wall paint (V69) = D69 * 0.375 — from "6 L / 16 m²" (6/16 = 0.375).

B.IV.2 (row 70) — "Painting on vaults" — unit , D70 = 315.6358139061665 (= D44). - Wall paint (V70) = D70 * 0.375

B.IV.3 (row 71) — "Glycero-based paint for metal carpentry/joinery" — unit u, D71 = 23. - Antirust paint (W71) = D71 * 5 — 5 L bucket per unit, flat, independent of the item's actual surface area.

B.IV.4 (row 72) — "Compressed Tyrolean plaster" — unit , D72 = 344.4213566576118 (= D41). E-note explicitly states: "Not in OMPU, brief proportions" — i.e. the modeler flags this recipe as not sourced from whatever "OMPU" reference document the rest of the sheet otherwise implicitly follows, but their own ad hoc estimate for a 3 mm coat. - Sand (I72) = D72 * 0.03 * 1400 / 16001400 kg sand/m³ plaster, converted to volume by dividing by an assumed sand density of 1600 kg/m³ (stated explicitly in a cell comment on I72: "density of sand is considered → 1600 kg/(m³)"). This is the only row in the sheet that converts a mass ratio into a volume via an explicit density constant — every other sand/gravel ratio elsewhere is already given directly in m³/m³. Sand density is otherwise nowhere stated for the workbook's many other sand-mass-implied formulas (cement plaster's 0.88 m³/m³ etc. sidesteps the need for a density by working in volume ratios throughout) — see Factors table and Open Questions. - Water (K72) = D72 * 0.03 * 0.2200 L water/m³ plaster = 0.2 m³/m³, correctly converted. ⚠E18 (documentation-only): a cell comment on K72 states "1L=1000(m³)", which inverts the actual relationship (1 m³ = 1000 L). The formula itself is correct; only the explanatory comment is backwards. Flagged because a future maintainer trusting the comment's stated relationship, rather than the formula, could introduce a real unit bug. - Cement (L72) = D72 * 0.03 * 400

V. Tiling

B.V.1 (row 74) — "Floor tiling" — unit , D74 = 217.02. - Floor tiles (X74) = D74 * 1.1 — 10% cutting/breakage waste factor, stated explicitly as "waste" in the E-note.

B.V.2 (row 75) — "Tile skirting" — unit ml, D75 = 191.62. - Floor tiles (X75) = D75 * 0.1 * 1.1 — linear metres × 0.1 m skirting height × the same 1.1 waste factor, converting ml to an equivalent tiled m².

B.V.3 (row 76) — "Faience tiling" — unit , D76 = 39.6. - Floor tiles (X76) = D76 * 1.1

Totals row (row 77)

{col}77 = SUM({col}3:{col}76) for every material column G through X. This is a plain arithmetic sum with no additional logic — the project's total material quantities.


Unit conventions & conversions

Input units (column C)

Unit Meaning Used for
cubic metre volumetric works (concrete, earthwork, masonry counted by volume)
square metre area-based works (walls, plaster, paint, tiling, roofing)
ml mètre linéaire (linear metre) length-based works (footings run, gutters run, parapet coping, film barrier)
u / ud unité (piece count) — the Unit column always uses u; the E-note prose sometimes writes ud informally discrete items (windows, doors, arches, gutters)
ff forfait (lump sum) one row only (A.IX.2) — a flat allowance not tied to a measured quantity

Output (material) units — columns G–X

Col Material Unit Notes
G Rock
H Gravel
I Sand Row 72 is the sole exception where a mass ratio (kg sand/m³ plaster) is converted to this volume unit via an explicit 1600 kg/m³ density assumption.
J Earth Raw/loose earth for adobe, rammed/compacted earth, earth mortar joints, earth plaster.
K Water
L Cement kg
M Steel Rebars kg Mass-based.
N Steel plates Volume-based — see unit-convention inconsistency below.
O Steel profiles Volume-based — see unit-convention inconsistency below.
P Polyethylene film
Q Bitumen kg
R Crude oil L E-note for its only use (row 42) calls it "Petrol"; column header calls it "Crude oil" — see vocabulary note below.
S Waste Oil L Header text has a typo: Waste Oil((L) — double open-parenthesis (E22, cosmetic).
T Glass
U Anti-termite L
V Wall paint L
W Antirust paint L
X Floor Tiles

Notable unit-convention inconsistency: steel is tracked in two incompatible bases

Reinforcement steel (rebar, column M) is tracked by mass (kg), which is standard practice — rebar is bought, specified, and typically carbon-costed by weight. Fenestration steel (window/door frame plates and profiles, columns N and O) is tracked by volume (m³) instead. This means:

  1. The two steel material streams cannot be summed or compared without a density conversion.
  2. No steel density value appears anywhere in this workbook (contrast with sand, which does get an explicit density in row 72's comment). If a downstream module needs total steel mass, or a single "Steel" MaterialCategory bucket, that conversion factor is missing from the source and must come from Marco, not be assumed by this module. (Common engineering references put mild/structural steel density around 7,850 kg/m³, but that number does not appear in this workbook and must not be silently inserted into the material-quantities pipeline — see Open Questions.)

Conversions found (explicit and implicit)

  • Mass ↔ volume, sand: 1600 kg/m³ (row 72 only; stated in a cell comment).
  • Volume ↔ volume, L ↔ m³: 1 m³ = 1000 L. Applied correctly wherever needed (e.g. row 72's water: 200 L/m³ → 0.2 m³/m³), but the one place this relationship is written out in prose (the K72 comment) states it backwards (E18).
  • Linear metres → area: ml × (assumed depth/height) = m², used twice: row 8 (ml × 1 m roll depth), row 75 (ml × 0.1 m skirting height).
  • Area × thickness → volume: used throughout the metal-carpentry section (steel sheet/profile/glass volumes = area or perimeter × a stated thickness in metres, always 0.004 m = 4 mm for plate/glass).
  • No literal "m³ column populated in litres" instance was found on close inspection of every G–X column (this class of bug was specifically checked for, given the task brief's example). The closest relative is the backwards L↔m³ comment text on K72 (E18) — the calculation itself is unit-correct.

Factors table

Split into (a) material composition / yield ratios — reusable, dosage- or recipe-level constants that a general-purpose quantities engine should centralise — and (b) wastage / loss factors proper. Project-specific geometric take-offs (bricks per m² for a particular wall detail, window dimensions, vault radii) are not included here; they are per-BoQ-line inputs specific to the Keur Songho reference project, not general factors — see Open Questions on where they should live in the object model.

For every factor: value · where used · source, as literally stated (or not stated) in the workbook. Per the ground rule, unsourced factors are marked PROVENANCE UNKNOWN rather than assigned an invented origin.

(a) Material composition / yield ratios

Factor Value Used in (rows) Source stated in workbook?
Concrete @ 350 kg/m³: gravel 0.67 m³/m³ 9, 11, 12 No — PROVENANCE UNKNOWN (workbook implies an external mix-design reference, possibly "OMPU" — see below; not vendored)
Concrete @ 350 kg/m³: sand 0.53 m³/m³ 9, 11, 12 PROVENANCE UNKNOWN
Concrete @ 350 kg/m³: water 0.19 m³/m³ (0.01+0.18) 9, 11, 12 PROVENANCE UNKNOWN
Concrete @ 350 kg/m³: rebar 100 kg/m³ 9, 11, 12 PROVENANCE UNKNOWN
Concrete @ 300 kg/m³: gravel 0.72 m³/m³ 51 PROVENANCE UNKNOWN
Concrete @ 300 kg/m³: sand 0.54 m³/m³ 51 PROVENANCE UNKNOWN
Concrete @ 300 kg/m³: water 0.15 m³/m³ 51 PROVENANCE UNKNOWN
Concrete @ 250 kg/m³: gravel 0.75 m³/m³ 6 (×0.21, see E2), 53 (as 0.45, buggy — E10), 55 PROVENANCE UNKNOWN
Concrete @ 250 kg/m³: sand 0.53 m³/m³ 6, 53, 55 PROVENANCE UNKNOWN
Concrete @ 250 kg/m³: water 0.13 m³/m³ (also 0.01+0.45*0.21 variant in row 5, and 0.01+0.08*0.21 in row 6 — not a single flat number in that context) 6, 53, 55 PROVENANCE UNKNOWN
Concrete @ 150 kg/m³ (blinding): gravel 0.56 m³/m³ 7 PROVENANCE UNKNOWN
Concrete @ 150 kg/m³ (blinding): sand 0.38 m³/m³ 7 PROVENANCE UNKNOWN
Concrete @ 150 kg/m³ (blinding): water 0.08 m³/m³ 7 PROVENANCE UNKNOWN
Cement plaster (16 mm coat): cement 400 kg/m³ plaster 41, 43, 45*, 46*, 47* (*formula missing — E13) PROVENANCE UNKNOWN
Cement plaster (16 mm coat): sand 0.88 m³/m³ plaster 41, 43, 45, 46, 47 PROVENANCE UNKNOWN
Cement plaster (16 mm coat): water 0.2 m³/m³ plaster 41, 43, 45, 46, 47 PROVENANCE UNKNOWN
Coat thickness (cement plaster) 0.016 m 41, 43, 44, 45, 46, 47 Stated directly in each row's own E-note ("16mm plaster") — a design/spec parameter, not an empirical yield factor
Earth-sand plaster on vaults: sand 0.84 m³/m³ plaster 44 PROVENANCE UNKNOWN
Earth-sand plaster on vaults: earth 0.24 m³/m³ plaster 44 PROVENANCE UNKNOWN
Earth-sand plaster on vaults: water 0.3 m³/m³ plaster 44 PROVENANCE UNKNOWN
Earth-sand-gravel-tar roof coating (per m³ of 0.042 m coat): sand 0.32 m³/m³ 42 PROVENANCE UNKNOWN
" : earth 0.11 m³/m³ 42 PROVENANCE UNKNOWN
" : water 0.3 m³/m³ 42 PROVENANCE UNKNOWN
" : bitumen 31 kg/m³ 42 PROVENANCE UNKNOWN
" : crude oil/"petrol" 5.5 L/m³ 42 PROVENANCE UNKNOWN
" : waste oil 11 L/m³ 42 PROVENANCE UNKNOWN
Coat thickness (earth-sand-gravel-tar roof) 0.042 m 42 Stated in-sheet as the coat thickness
Adobe brick: earth content 0.0143 m³ earth/brick 14–19, 21–27, 32, 33, 35, 38 PROVENANCE UNKNOWN
Adobe brick: water content 0.0043 m³ water/brick same rows PROVENANCE UNKNOWN
→ implied brick water:earth ratio 30.07% (0.0043÷0.0143) (derived) Internally consistent across every wall/arch row — see also the different 25.85% ratio used in vault rows
BBSC brick: rock content 0.0002 m³ rock/brick 15, 17, 32, 38 PROVENANCE UNKNOWN. Note: this is the only material attributed to a BBSC brick anywhere in the sheet — no cement, binder, or earth content is modeled for it (see Open Questions / new-category discussion)
Earth mortar joint: earth 1.5 m³ raw earth / m³ finished joint (or compacted fill) 5 (via 1.5), 14–19, 21–27, 32, 33, 35, 38 (all the ×1.5 joint terms), 50 (compacted fill, direct ×1.5), 57 (slope, ×1.5) PROVENANCE UNKNOWN — functions as a raw-to-compacted bulking/shrinkage factor
Earth mortar joint: water 0.45 m³ water / m³ finished joint (or compacted fill) same rows (all the ×0.45 joint terms), 50, 57 PROVENANCE UNKNOWN
Vault brick water:earth-volume ratio 0.2585 (derived from 0.0043 ÷ (0.42×0.22×0.18)) 29, 30 Derivation shown in-sheet (row 29's E-note); the three brick dimensions (0.42, 0.22, 0.18 m) themselves are PROVENANCE UNKNOWN
Vault brick thickness 0.25 m (mislabeled "0.25cm" in text — E19) 29, 30 PROVENANCE UNKNOWN
Sand density (mass↔volume) 1600 kg/m³ 72 only Stated explicitly in a cell comment: "density of sand is considered → 1600 kg/(m³)" — the only sourced-in-sheet density value in the whole workbook, and even this has no external citation beyond the modeler's assertion
Tyrolean plaster (3 mm coat): cement 400 kg/m³ 72 PROVENANCE UNKNOWN; E-note flags the whole row as "Not in OMPU, brief proportions" — i.e. explicitly not from whatever the sheet's implicit main reference is, but the modeler's own approximation
Tyrolean plaster: sand (mass) 1400 kg/m³ 72 PROVENANCE UNKNOWN, "brief proportions"
Tyrolean plaster: water 200 L/m³ 72 PROVENANCE UNKNOWN, "brief proportions"
Wall paint coverage 6 L / 16 m² (= 0.375 L/m²) 69, 70 PROVENANCE UNKNOWN
Antirust paint 5 L per carpentry unit 71 PROVENANCE UNKNOWN
Anti-termite treatment rate 5 L / m² 36 Stated directly in-sheet ("rate = 5l/m2") but no external citation
Steel plate thickness (window/door sheet) 0.004 m (4 mm) 60, 61, 63, 64, 65, 67 Stated directly in-sheet
Steel profile section 5 × 5 × 0.4 cm hollow square, steel cross-section = 4 × 0.05 × 0.004 m² per metre of profile 60, 61, 63, 64, 65 Stated directly in-sheet
Glass thickness 0.004 m (4 mm) 60, 63, 64 Stated directly in-sheet

"OMPU": Rows 72's E-note explicitly contrasts its own "brief proportions" with a reference the rest of the sheet implicitly follows, abbreviated "OMPU". This acronym is never expanded anywhere in the workbook. It could not be reliably identified from public sources in the time available for this analysis (searches for Senegalese/ West African BTP devis-estimatif conventions did not turn up an unambiguous match) — see Open Questions. Every factor in this table not attributed to an in-sheet comment should be assumed to originate from "OMPU," whatever that is, until Marco confirms or supplies the actual reference document.

(b) Wastage / loss factors

Factor Value Applies to Source
Tile cutting/breakage waste 1.1 (10%) Floor tiling, tile skirting, faience tiling (rows 74, 75, 76) Stated in-sheet as "waste" (E-note: "Tiles = m2 * 1.1 waste"); no external citation — PROVENANCE UNKNOWN
Steel sheet cutting/waste factor 1.3 (30%) Window/door steel plate area (rows 60, 61, 63, 64*, 65) Stated directly in each E-note as part of the surface formula; no external citation — PROVENANCE UNKNOWN. (*Row 64 applies 2.3 instead due to E12.)
Raw-to-compacted earth bulking factor 1.5 Every earth quantity in the workbook (bricks, joints, compacted fill, slope) Not labeled "waste" explicitly anywhere, but functions as one (loose/raw material needed per unit of finished, compacted volume) — PROVENANCE UNKNOWN
Foundation layer-split normalizer /0.95 Rows 5 & 6 only Arithmetic normalizer (50 cm + 45 cm = 0.95 m), not a wastage factor — included here only to pre-empt it being mistaken for one
Concrete-fraction-of-foundation-volume "0.21" vs stated "20%" 0.21 vs 0.20 Rows 5, 6 Unexplained — see E2. Might be an intentional small extra-mortar/voids allowance functioning like a waste factor, but this is speculation; PROVENANCE UNKNOWN either way

french_devisMaterialCategory mapping

Caveat, stated up front: this analysis has no access to the actual MaterialCategory StrEnum definition in the chwr_LCA repo. The mapping below proposes target concepts, named descriptively, for each material this workbook produces. Marco should reconcile these against the real enum's existing member names before implementation — the grouping (which workbook outputs are the same category vs. need to stay distinct) is the substantive claim here; the literal Python identifier is not.

The French vocabulary column reflects terms as they actually appear in the sheet's E-column conversion notes (which retain original French/francophone-BTP terms even where the visible column headers were translated to English) and in devis practice more broadly (ud, ml, ff, "forfait").

Source column / vocabulary French devis term seen in-sheet Proposed MaterialCategory Fit?
Rock (G) "Rock" / implied "pierre" or "moellon" (cyclopean masonry stone) AGGREGATE_COARSE or a dedicated ROCK_STONE Likely clean fit against a conventional-construction taxonomy
Gravel (H) "Gravel" / "gravier" AGGREGATE_COARSE (or a GRAVEL member if the enum splits aggregate grades) Clean fit
Sand (I) "Sand" / "Sable" (used verbatim in several E-notes, e.g. row 6's "Sable(sand)") AGGREGATE_FINE / SAND Clean fit
Earth (J) "Earth" / implied "terre" (raw soil for adobe, banco, rammed earth, earth mortar) No clean fit expected — see proposed new member below Poor fit against a conventional-construction-oriented taxonomy
Water (K) "Water" / "eau" WATER Clean fit
Cement (L) "Cement" / "Ciment" CEMENT Clean fit
Steel Rebars (M) "Steel Rebars" / "Aciers" ("fer à béton") STEEL_REBAR Clean fit
Steel plates (N) "Steel plates" / "tôle" (sheet steel) STEEL_SHEET (mass-basis reconciliation needed — see Open Questions) Fit exists but unit basis (m³ here vs. conventionally kg) needs resolving
Steel profiles (O) "Steel profiles" / "profilés" STEEL_PROFILE or STEEL_SECTION Same unit-basis caveat as above
Polyethylene film (P) "polyethylene film" / "polyane" (used verbatim in the row 8 description: "Capillary barrier – polyane film") PLASTIC_FILM or WATERPROOFING_MEMBRANE, if either exists Plausible fit against a generic plastics/membrane category; flag if neither exists
Bitumen (Q) "bitumen" / "bitume" BITUMEN Clean fit
Crude oil (R) "Crude oil" (column header) vs. "Petrol" (the row 42 E-note's own word for the same column) PETROLEUM_DISTILLATE or similar Fit plausible, but the vocabulary mismatch itself (crude oil vs. petrol vs. possibly diesel — used here as a thinning agent for a bituminous roof coat) should be resolved with Marco before locking a category name, since "crude oil" and "petrol/diesel" are materially different LCA inputs
Waste Oil (S) "Waste Oil" / "huile de vidange" (recycled/used motor oil, reused as a sealant) No clean fit expected — see proposed new member below Recycled/waste inputs typically carry a different LCA burden-allocation treatment than virgin material and usually need their own category
Glass (T) "Glass" / "Verre" GLASS Clean fit
Anti-termite (U) "Anti-termite" / "produit anti-termite" PESTICIDE or CHEMICAL_TREATMENT Plausible fit if such a category exists; else propose new
Wall paint (V) "Wall paint" / "peinture" PAINT_COATING Clean fit
Antirust paint (W) "Antirust paint" / "peinture antirouille", "peinture glycéro" PAINT_COATING (same category as Wall paint, distinguished by an attribute, not a separate enum member) or a distinct PROTECTIVE_COATING if the taxonomy separates decorative from anti-corrosion coatings Fit depends on how granular the existing taxonomy already is
Floor Tiles (X) "Floor Tiles" / "carrelage", "faïence" (the sheet distinguishes floor tiling from wall "faience tiling" in its descriptions, but both feed the same output column) CERAMIC_TILE Clean fit — but note the workbook itself collapses floor tile and wall faience into one material column; if MaterialCategory distinguishes them, this collapsing is a modeling choice inherited from the source, not something this spec can resolve without Marco's input
"BBSC brick" (embedded in rows 15, 17, 32, 38 — never its own output column, contributes only to Rock) "BBSC" (acronym never expanded in-sheet) No fit at all under the current model — the workbook already forces this into ROCK; see below Forced, poor fit — flagged as its own item, not a translation problem

Where no MaterialCategory fits — proposed new members

  1. RAW_EARTH (or EARTH_SOIL, naming to be reconciled with repo conventions). Justification: This project's stated focus is earth-based construction (adobe, CEB, CSEB) benchmarked against concrete/CHB and RC frame. Raw earth/soil is the single largest-volume material in this reference building by a wide margin (830.5 m³ in the project total — more than 10× the next largest volumetric material, Rock at 70.9 m³). A conventional-construction-oriented MaterialCategory enum, built around concrete, steel, masonry block, and timber, is very unlikely to already contain a member for unfired, unstabilised raw soil used as a structural/enclosure material in its own right (as opposed to, say, fill or landscaping earth). Forcing it into an existing AGGREGATE or FILL category would misrepresent both its structural role and its LCA profile (near-zero processing energy, but often non-trivial transport and, depending on sourcing, land-use burden) relative to quarried aggregate.

  2. STABILIZED_EARTH_BLOCK (or a name matching the project's own CEB/CSEB terminology) — for the BBSC brick type referenced in rows 15, 17, 32, and 38. Justification: this is the clearest case in the whole workbook of a material being forced into the wrong bucket rather than left unmapped. The workbook already commits this exact error: BBSC bricks are represented solely by an assumed rock content (0.0002 m³ rock/brick), with no cement/stabiliser, no earth, and no water accounted for their production, despite "BBSC" almost certainly denoting some kind of cement- or lime-stabilised compressed-earth block (a CSEB analog) — i.e. a fundamentally different material system from both plain adobe and from quarried rock. Treating a stabilised earth block's embodied impact as if it were equivalent to raw rock is very unlikely to be defensible for an LCA tool. This should become its own category (or, at minimum, its own Material composition recipe distinct from both RAW_EARTH and ROCK) rather than being silently absorbed into Rock the way the source workbook does it. What "BBSC" actually stands for, and what its true material composition is, is unresolved — see Open Questions.

  3. WASTE_OIL as distinct from any virgin-petroleum category. Justification: row 42 explicitly separates "Crude oil"/petrol (a virgin input, used as a thinning agent) from "Waste Oil" (explicitly reused/recycled motor oil, used as a sealant in the same traditional roof coating). LCA methodology generally treats reused/recycled inputs with a different burden-allocation approach (e.g. cut-off or avoided-burden allocation) than virgin material extraction. Collapsing these into one petroleum-products category would erase a distinction the source data itself makes deliberately, and would misstate this technique's actual environmental profile — one of its notable features is precisely that it reuses waste oil rather than consuming only virgin material.

No other output column appears to need a new member — Rock, Gravel, Sand, Water, Cement, Steel Rebar, Glass, Bitumen, Paint, and Tile are all conventional construction materials any general-purpose MaterialCategory taxonomy is likely to already cover.


Parity fixture (inputs → expected outputs)

All expected values below are the workbook's own cached formula results (data_only=True read of the delivered .xlsx), i.e. exactly what Excel computes today — bugs included. A correct Python port must reproduce these to within 0.1%. Do not "improve" any of the flagged formulas when building this fixture; a corrected-formula version would legitimately fail this test and that is by design — fixing the source data is a separate, explicit decision for Marco, not something this module should do silently.

Fixture 1 — clean "golden path" line: 350 kg/m³ reinforced concrete footing

Line A.III.4 (row 9): "Reinforced concrete for isolated footing, dosed at 350 kg/m³ (1.2 x 1.2 x 0.4 m)"

Input

N°:        A.III.4
Unit:      m³
Quantity:  4.608

Expected output | Material | Formula | Expected value | |---|---|---| | Gravel | 4.608 * 0.67 | 3.08736 m³ | | Sand | 4.608 * 0.53 | 2.44224 m³ | | Water | 4.608 * (0.01 + 0.18) | 0.87552 m³ | | Cement | 4.608 * 350 | 1612.8 kg | | Steel Rebars | 4.608 * 100 | 460.8 kg | | (all other material columns) | — | 0 |

Fixture 2 — earth-based line: adobe wall, 80 cm thick

Line A.V.1 (row 14): "Walls made of adobe bricks, 80 cm thick, (Height 2.52 m, 14 courses)"

Input

N°:        A.V.1
Unit:      m²
Quantity:  70.308

Expected output | Material | Formula | Expected value | |---|---|---| | Earth | 70.308 * ((55.56 * 0.0143) + (0.3 * 1.5)) | 87.49886846 m³ | | Water | 70.308 * ((55.56 * 0.0043) + (0.3 * 0.45)) | 26.28872366 m³ | | (all other material columns) | — | 0 |

Fixture 3 — a line with a preserved bug: parapet coping (gravel width typo, E10)

Line B.II.2.a (row 53): "Concrete slabs (tiles) on parapet walls"

Input

N°:        B.II.2.a
Unit:      ml
Quantity:  77.68

Expected output (as Excel actually computes it — includes the E10 bug) | Material | Formula (as written) | Expected value | Note | |---|---|---|---| | Gravel | 77.68 * 0.45 * 0.04 * 0.75 | 1.04868 m³ | ⚠ E10 — cross-section width should be 0.35 per the row's other three coefficients; a "corrected" implementation would yield 0.81564 instead and must not be used for this parity test | | Sand | 77.68 * 0.35 * 0.04 * 0.53 | 0.5763856 m³ | | | Water | 77.68 * 0.35 * 0.04 * 0.13 | 0.1413776 m³ | | | Cement | 77.68 * 0.35 * 0.04 * 250 | 271.88 kg | | | (all other material columns) | — | 0 | |

Fixture 4 — whole-project aggregate (row 77, all 74 line items summed)

Input: the complete "Keur Songho School Complex" BoQ, all 74 line items exactly as transcribed in Formulas (transcribed) above.

Expected output (project totals, SUM(col3:col76) per material column) | Material | Unit | Expected total | |---|---|---| | Rock | m³ | 70.86880231 | | Gravel | m³ | 31.06387539 | | Sand | m³ | 56.05015412 | | Earth | m³ | 830.4936249 | | Water | m³ | 299.5835496 | | Cement | kg | 23301.04701 | | Steel Rebars | kg | 1330.6 | | Steel plates | m³ | 0.3077544 | | Steel profiles | m³ | 0.190992 | | Polyethylene film | m² | 139.6 | | Bitumen | kg | 376.475904 | | Crude oil | L | 66.794112 | | Waste Oil | L | 133.588224 | | Glass | m³ | 0.0096084 | | Anti-termite | L | 1445.75 | | Wall paint | L | 327.6750315 | | Antirust paint | L | 115 | | Floor Tiles | m² | 303.3602 |

This aggregate fixture is the most important regression check: it can only pass if every line item's formula (bugs included) was ported correctly, since a single mis-transcribed row will shift a column total by more than 0.1% in most cases (the BoQ has few enough lines, and several material columns few enough contributing rows, that errors don't average out).


Errors & inconsistencies found

Numbered for cross-reference from the Formulas section above. Severity is about magnitude of numeric impact on the affected line, not about how likely the bug is to matter for the final LCA result (that depends on the material's overall contribution, which this document doesn't attempt to judge).

ID Location Issue Numeric impact
E1 D5, D6 Both derive from a hardcoded external constant 82.412 with no in-sheet derivation. PROVENANCE UNKNOWN. N/A (input, not a bug per se, but untraceable)
E2 Rows 5, 6 Formulas use a 0.21 concrete-fraction factor; both rows' own descriptions state the fraction is 20% (0.20). Unexplained 1-point, systematic discrepancy. ~5% relative, on every output of both rows
E3 K16 References D15 (previous row's quantity, 40.176) instead of D16 (this row's own quantity, 19.152). Water for A.V.3 is 2.10× what it should be if computed from its own row's quantity
E4 J24, K24 Missing outer parentheses: D24*(9.9*0.0143)+(0.03*1.5) instead of D24*((9.9*0.0143)+(0.03*1.5)). The joint term is added as a flat constant, not scaled by quantity. Earth understated ~22%, water understated ~22%, for A.VI.4 (row 24)
E5 J38 Same missing-parentheses bug as E4. (K38 on the same row does not have the bug — inconsistent even within one row.) Earth understated ~21.5% for A.X.1
E6 K32 Coefficient 0.043 used where every other equivalent formula in the sheet uses 0.0043 (factor-of-10 decimal-point slip). Water overstated 7.6× for A.VIII.1 — largest single numeric distortion found
E7 J29 (and its dependent K29) Hardcodes the line's own quantity (18.6) instead of referencing D29. Numerically inert today (D29 == 18.6) but will silently desync if D29 is ever edited. None currently; latent
E8 K30 Computes water as D30 * 0.2585 (raw linear-metre input × ratio), not J30 * 0.2585 (computed earth volume × ratio) — breaking the method the sheet documents for this exact recipe one row above (K29 = J29 * 0.2585) and restates in this row's own E-note. Water understated by more than half versus the sheet's own documented method (13.80 m³ as written vs. 28.17 m³ if the documented method were applied)
E9 U36 Hardcodes 289.15, a manually re-typed rounding of D35 (289.152), instead of referencing it. Negligible today (~0.001%) but a fragile, non-referential pattern
E10 H53 Uses cross-section width 0.45 where the other three formulas on the same row (and the row's own description) use 0.35. Gravel overstated ~28.6% for B.II.2.a
E11 O60 Perimeter term uses 0.2*0.9 where the row's own E-note and every sibling row uses 2*0.9 (decimal-point slip). Steel profile volume understated ~17.2% for B.III.1.a
E12 N64 Uses 2.7 and 2.3 where the row's own E-note documents 2.79 and 1.3 (a dimension rounding slip stacked with a waste-factor typo). Steel plate volume overstated 1.71× for B.III.2.b
E13 L45, L46, L47 (missing entirely) Rows 45, 46, 47 ("Plastering on columns/beams/window reveals") each explicitly document a cement dosage in their E-note, identical to rows 41/43, but no cement formula was ever entered. Cement demand for these 3 lines is silently absent from the project total — not a wrong number, a missing one
E14 Row 42 Description ("earth-sand-gravel-tar") names gravel as a constituent; neither the E-note nor any formula computes a gravel quantity for this row. Gravel for this line silently absent
E15 C27 Unit column contains the full line-item description text instead of a unit code. Sibling rows and the row's own E-note ("ud *…") indicate the intended value is u. No numeric impact (D27's formulas don't read C27), but breaks any unit-based validation/parsing of this column
E16 Rows 24 & 25 Both carry A.VI.4 despite describing two different works ("90 cm arches on 38 cm thick walls" vs. "40 cm arches on 38 cm thick walls"). Distinguish from the legitimate composite-item pattern in rows 5–6 (same code, same described item, split by layer). No numeric impact on totals (row 77 sums by physical row, not by code), but breaks any downstream logic that groups or looks up by expecting uniqueness
E17 Comment anchored at AW43 (semantically row 43, "Interior cement plaster on walls") Unresolved reviewer note: "A vérifier avec Adama" ("to check with Adama"), dated 2024‑02‑08, never actioned before delivery. Unknown — the note gives no detail on what specifically needs checking
E18 Comment on K72 States "1L=1000(m³)", which inverts the true relationship (1 m³ = 1000 L). The formula itself is correct; only the comment is wrong. None on current output; risk to future maintainers who trust the comment over the formula
E19 E-note text, rows 29/30 "(0.25cm brick)" almost certainly means 0.25 m (the arithmetic only works as metres); as written the label describes a physically implausible 2.5 mm brick. Documentation/label only — the formula itself correctly uses 0.25 as metres
E20 Row 63 Title states door dimensions "1.2 x 3.12 m"; the E-note and every formula in the row consistently use 3.2 m. Internally self-consistent, but the description disagrees with its own calculation by 8 cm. None on current output (calc is self-consistent); a documentation accuracy issue
E21 C7, C9, C11, C12 Unit strings carry a trailing space ('m³ ' vs. 'm³' elsewhere). None on values; will break naive string-equality unit validation unless stripped
E22 Header row, column S Waste Oil((L) — a literal double-open-parenthesis typo in the column header text. Cosmetic only

Open questions for Marco

  1. "OMPU" (row 72's E-note: "Not in OMPU, brief proportions") — what document is this? Every "PROVENANCE UNKNOWN" factor in this spec is presumably sourced from whatever OMPU is; identifying and (if licensing allows) obtaining it would let this spec's entire Factors table move from "unsourced but internally consistent" to properly cited, which the project's stated bar for defensibility (an unsourced impact factor is called out explicitly as a "defensibility failure") makes fairly urgent.

  2. What does "BBSC" stand for, and what is its actual material composition? The workbook models it as pure rock (0.0002 m³/brick) with no binder, no earth, no water. If BBSC is a cement- or lime-stabilised compressed earth block (the working hypothesis in this spec, offered as a hypothesis only — not confirmed anywhere in the source), its true LCA profile is materially different from quarried rock, and the proposed STABILIZED_EARTH_BLOCK category (or whatever recipe replaces it) needs a real composition, not the rock-only placeholder inherited from the spreadsheet.

  3. Row 30's water formula (E8): should the Python port replicate the as-written value (D30 * 0.2585 — smaller, undocumented-method), or the value the sheet's own stated method would produce (J30 * 0.2585 — larger, consistent with row 29 and with this row's own E-note)? This is the single largest unresolved numeric question in the workbook. The parity fixture above uses the as-written value; Marco should confirm whether that's actually what should ship, or whether this is exactly the kind of error the Python port should quietly not reproduce (in which case the fixture needs to be rebuilt against a corrected value, and this decision should be documented, not silent).

  4. Steel unit basis: reinforcement steel is tracked by mass (kg), fenestration steel by volume (m³), with no density anywhere in the workbook to reconcile them. Does MaterialCategory need a single STEEL bucket (requiring Marco to supply/approve a density), or should these remain distinct volume- and mass-basis categories permanently?

  5. Sand density (1600 kg/m³, row 72 only): is this meant to be a project-wide constant (i.e., should it be pulled out and reused anywhere else a sand mass ratio shows up, even though nowhere else in this sheet currently needs it), or is it specific to the "not in OMPU" Tyrolean-plaster estimate and shouldn't be treated as authoritative for anything else?

  6. Glass quantities scaled by distinct-opening-type, not by unit count (rows 60, 63, 64 — see the discussion under B.III.1.a in Formulas): is it intentional that a window type with D60 = 17 units only contributes glass for one window, or is this an undercounting bug (should glass have been D60 * (1/2 * 0.45² * π * 2 * 0.004), 17× larger)? This has real numeric weight — it's the difference between 0.0025 m³ and 0.043 m³ of glass for that single line, and the project total for Glass (0.0096 m³) would change substantially either way.

  7. Should the multi-row composite pattern (A.III.1 split across rows 5–6) be represented in the domain model as one Assembly with an internal list of material "layers," or as two independent Assembly instances that happen to share a devis code? The source data supports either reading; the choice affects how gets used (or not used) as a natural key anywhere in the Python object model, and is worth deciding before E16 (the genuine duplicate-code bug) needs a validation rule, since the validation rule differs depending on whether repeated codes are ever legitimate.

  8. Where should the ~150 project-specific geometric take-off constants live (bricks per m² for each of the wall/arch/vault details in this specific building, window and door dimensions, vault radii, etc.)? They are not general LCA factors — they're specific to how this building's earthen walls, arches, and vaults were detailed. Options include: (a) baking them into per-Assembly-type "recipe" definitions in code (reproducing the spreadsheet's structure, one recipe per named wall/arch/vault type), (b) computing them from more primitive geometric parameters (course count, brick dimensions, wall thickness) passed in per instance, or (c) treating them as user-supplied inputs alongside quantity, unit, and description. (b) is the most general and would generalize best beyond this one reference project, but is a bigger engineering lift and isn't validated by anything in this source workbook (which only ever shows the pre-multiplied per-m²/per-unit constant, never the underlying brick count × brick volume derivation as separate cells). Recommend Marco weigh in before implementation starts, since this choice shapes the Assembly schema.

  9. Crude oil vs. "Petrol" vs. potentially diesel (row 42, column R): the column header and the row's own conversion note use different words for the same material. Confirm what liquid this actually is (crude oil, refined petrol/gasoline, diesel, and kerosene are all materially different for LCA purposes) before locking a MaterialCategory name or an emissions factor onto it downstream.

  10. ff (forfait / lump sum) as a unit: only one line item in this reference project uses it (A.IX.2, anti-termite treatment). Is ff meant to be a legitimate long-term member of UnitEnum, or is it a modeling shortcut specific to this one line that should be replaced with a proper quantity-driven formula (the way every other line in the sheet works) before this becomes a general-purpose module? As implemented in the source, a ff-unit line's material output is entirely dependent on a hardcoded external quantity (289.15, itself a copy of another row's D value — see E9) baked into the formula rather than into the line's own Quantity field, which doesn't fit the "everything flows from D" contract the rest of the sheet establishes.