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 A–E are
input/description, columns G–X are computed output (material quantities).
Column F is an always-empty spacer column.
| Col | Header (row 2) | Type | Description |
|---|---|---|---|
| A | N° |
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 m³, m², 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 G–X (see Errors section). Treat it as the stated intent; treat the G–X 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
Dis empty, columnAbold, 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 hierarchicalN°prefix used by line items below them. - Line item row: column
Dis populated (formula or literal), columnAnot 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 N° 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 N° 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¶
N°— a devis code (free text in this source; not validated).Description of Works— free text.U— one of{m³, m², ml, u, ff}(must be validated on ingestion; the source file itself fails this validation once — see E15).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:
K16depends onD15, notD16(wrong-row reference bug, E3).K29depends onJ29(the row's own computed Earth quantity), not directly onD29— this is the documented method (water is a fraction of earth volume, not of the raw linear-metre input).K30depends onD30directly, not onJ30— breaking the patternK29establishes one row above it for what the sheet's own comment says is the same method (E8).J29andK29's chain effectively hardcodes the line's own quantity (18.6) instead of referencingD29(E7) — numerically inert here only becauseD29 == 18.6.U36hardcodes a rounded copy ofD35(289.15instead of289.152/=D35) rather than referencing it (E9).- Row 77 (
sum) depends on the full column rangerow3:row76for 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 m³, split across two rows.
D5 = 82.412 * 0.5 / 0.95→43.37473684.82.412is an external total (not derived anywhere in this sheet — E1, PROVENANCE UNKNOWN);0.5/0.95and0.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.84Earth (J5) = D5 * 1.5 * 0.21Water (K5) = D5 * (0.01 + (0.45 * 0.21))
A.III.1 (row 6) — second layer of the same composite item, unit m³.
D6 = 82.412 * 0.45 / 0.95→39.03726316.Rock (G6) = D6 * 0.84Gravel (H6) = D6 * 0.53 * 0.21Sand (I6) = D6 * 0.75 * 0.21Water (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
m³ (trailing space, E21), D7 = 0.576.
Gravel (H7) = D7 * 0.56Sand (I7) = D7 * 0.38Water (K7) = D7 * 0.08Cement (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 m³ (trailing space), D9 = 4.608.
Gravel (H9) = D9 * 0.67Sand (I9) = D9 * 0.53Water (K9) = D9 * (0.01 + 0.18)Cement (L9) = D9 * 350Steel 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 m³,
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 m³, 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 m², 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 m², 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 m², 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 m², 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 m², 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 m², 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 N° 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 m², 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 m², 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 m², 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 m²,
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 m², 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 m², 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 m², 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 m², 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 m², 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 m³, 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 m³, 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 m³, 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 m², 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 m², 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 m², 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 / 1600 — 1400 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.2 — 200 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 m², 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 m², 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 |
|---|---|---|
m³ |
cubic metre | volumetric works (concrete, earthwork, masonry counted by volume) |
m² |
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 | m³ | |
| H | Gravel | m³ | |
| I | Sand | m³ | 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 | m³ | Raw/loose earth for adobe, rammed/compacted earth, earth mortar joints, earth plaster. |
| K | Water | m³ | |
| L | Cement | kg | |
| M | Steel Rebars | kg | Mass-based. |
| N | Steel plates | m³ | Volume-based — see unit-convention inconsistency below. |
| O | Steel profiles | m³ | Volume-based — see unit-convention inconsistency below. |
| P | Polyethylene film | m² | |
| 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 | m³ | |
| U | Anti-termite | L | |
| V | Wall paint | L | |
| W | Antirust paint | L | |
| X | Floor Tiles | m² |
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:
- The two steel material streams cannot be summed or compared without a density conversion.
- 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"
MaterialCategorybucket, 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 (theK72comment) 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_devis → MaterialCategory 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¶
-
RAW_EARTH(orEARTH_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-orientedMaterialCategoryenum, 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 existingAGGREGATEorFILLcategory 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. -
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 ownMaterialcomposition recipe distinct from bothRAW_EARTHandROCK) 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. -
WASTE_OILas 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
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
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
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 N° 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 N° 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¶
-
"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. -
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_BLOCKcategory (or whatever recipe replaces it) needs a real composition, not the rock-only placeholder inherited from the spreadsheet. -
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 ownE-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). -
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
MaterialCategoryneed a singleSTEELbucket (requiring Marco to supply/approve a density), or should these remain distinct volume- and mass-basis categories permanently? -
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?
-
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 = 17units only contributes glass for one window, or is this an undercounting bug (should glass have beenD60 * (1/2 * 0.45² * π * 2 * 0.004), 17× larger)? This has real numeric weight — it's the difference between0.0025 m³and0.043 m³of glass for that single line, and the project total for Glass (0.0096 m³) would change substantially either way. -
Should the multi-row composite pattern (
A.III.1split across rows 5–6) be represented in the domain model as oneAssemblywith an internal list of material "layers," or as two independentAssemblyinstances that happen to share a devis code? The source data supports either reading; the choice affects howN°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. -
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 theAssemblyschema. -
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
MaterialCategoryname or an emissions factor onto it downstream. -
ff(forfait / lump sum) as a unit: only one line item in this reference project uses it (A.IX.2, anti-termite treatment). Isffmeant to be a legitimate long-term member ofUnitEnum, 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, aff-unit line's material output is entirely dependent on a hardcoded external quantity (289.15, itself a copy of another row'sDvalue — see E9) baked into the formula rather than into the line's ownQuantityfield, which doesn't fit the "everything flows fromD" contract the rest of the sheet establishes.