From 148d576d9d81d2521e32d088ca7323ec313354f1 Mon Sep 17 00:00:00 2001 From: Shankar Ramharack Date: Sat, 7 Nov 2026 20:51:31 -0400 Subject: [PATCH 1/2] Add transformer library with traceable sources --- docs/design-basis.md | 35 ++++++++++ docs/requirements.md | 35 +++++++--- src/kitlist/data/library/transformer_kva.csv | 65 +++++++++++++++++ src/kitlist/library.py | 38 ++++++++++ tests/test_library.py | 73 ++++++++++++++++++++ 5 files changed, 235 insertions(+), 11 deletions(-) create mode 100644 docs/design-basis.md create mode 100644 src/kitlist/data/library/transformer_kva.csv create mode 100644 src/kitlist/library.py create mode 100644 tests/test_library.py diff --git a/docs/design-basis.md b/docs/design-basis.md new file mode 100644 index 0000000..e729b1b --- /dev/null +++ b/docs/design-basis.md @@ -0,0 +1,35 @@ +# Design basis + +Every value in kitlist's equipment libraries traces to a cited source, so any +standard size can be checked against the document it came from. + +## How sources are recorded + +- Each library CSV in `src/kitlist/data/library/` has a `source` column holding a + reference ID from the table below. +- A test fails if a library uses an ID that isn't listed here, so no value can be + added without a source. +- Paywalled standards are cited by edition and clause. Their text is not reproduced. + +## References + +| ID | Document | Used for | Verified | Notes | +|---|---|---|---|---| +| DOE-431.196a | US 10 CFR 431.196(a)(2), low-voltage dry-type distribution transformers, table for units made 2016–2029 | `ieee-dry` kVA ratings | Against the regulation text, 2026-11-08 | Applies legally to 60 Hz units sold in the US; cited as a public record of the North American rating series. Free at ecfr.gov. | +| DOE-431.196b | US 10 CFR 431.196(b)(2), liquid-immersed distribution transformers, Table 5 | `ieee-pad` kVA ratings, limited to the scope of IEEE-C57.12.38 (single-phase, ≤ 250 kVA) and IEEE-C57.12.34 (three-phase) | Against the regulation text, 2026-11-08 | Same note as above. | +| IEC-60076-1 | IEC 60076-1:2011, Power transformers – Part 1: General, clause 5.1.2 (preferred values of rated power, R10 series of ISO 3) | `iec` kVA ratings | **No** | Values are the R10 preferred numbers. Paywalled; verify against a copy before relying on this family. | +| IEEE-C57.12.38 | IEEE C57.12.38-2025, pad-mounted, self-cooled, single-phase distribution transformers, 250 kVA and smaller | Scope of `ieee-pad` single-phase | Scope only, from the published abstract | Scope is 60 Hz; on 50 Hz projects, specify "in general accordance with, except 50 Hz". Paywalled. | +| IEEE-C57.12.34 | IEEE C57.12.34-2022, pad-mounted, compartmental-type, self-cooled, three-phase distribution transformers, 10 MVA and smaller | Scope of `ieee-pad` three-phase | Scope only, from the published abstract | Same note. Paywalled. | + +## Transformers + +Library: `transformer_kva.csv`, one row per standard rating, tagged by family (D7): + +- `ieee-dry`: low-voltage dry-type, North American series (DOE-431.196a). +- `ieee-pad`: liquid-filled padmount, North American series (DOE-431.196b), within the + scope of IEEE C57.12.38 (single-phase) and C57.12.34 (three-phase). +- `iec`: IEC series (IEC-60076-1). Not yet verified against the standard. + +These are *standard* ratings. Sizes local suppliers rarely stock (for example +10 kVA single-phase padmounts or 31.5 kVA IEC units) are still listed; trimming the +list to what is buyable is a separate, project-level decision. \ No newline at end of file diff --git a/docs/requirements.md b/docs/requirements.md index 27b9bbf..c7970a4 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -17,11 +17,13 @@ it exists. | REQ-02 | The layout is defined in one data file (`layout.yaml`). An invalid layout is rejected with a message naming the equipment type and the problem. | done (M2) | `test_grid.py` | | REQ-03 | Each table has a title row, a group row, a field-label row and a unit row; units are shown in square brackets. | done (M2) | `test_grid.py` | | REQ-04 | Each table is a named range (`SCH_`), so it can be selected in one step (Name Box) and copy-pasted into AutoCAD. v0.1 relies on manual copy-paste; AutoCAD Data Link and native table objects come after v0.1, using the same named ranges. | done (M2), paste trial pending | `test_writer.py` | -| REQ-05 | The schedules sheet prints landscape, fit to page width. Tables are not split across pages. | partial: page splits open | `test_writer.py` | -| REQ-06 | Equipment is sized as the smallest standard size covering load plus spare. | Python done; Excel in M4 | `test_sizing.py` | -| REQ-07 | Each row is checked against engineering rules; problems are shown beside the row. First rule: generator prime rating ≤ standby rating. | in progress (M0 exercise) | `test_checks.py` | +| REQ-05 | Each schedule sheet prints landscape, fit to page width. | done (M3) | `test_writer.py` | +| REQ-06 | Equipment is sized as the smallest standard size covering load plus spare. The spare margin is always stated explicitly. | Python done; Excel in M4 | `test_sizing.py` | +| REQ-07 | Each row is checked against engineering rules; problems are shown beside the row. First rule: a generator prime rating above standby is an error; prime equal to standby is allowed, with a note to confirm on the manufacturer's datasheet (D5). | in progress (M0 exercise) | `test_checks.py` | | REQ-08 | Project defaults (system voltages, frequency, indoor/outdoor enclosure) are entered once on a Project sheet. | M4 | | | REQ-09 | Branding comes from a profile file passed to the build; the public build carries no company branding. | M4 | | +| REQ-10 | Each equipment type has its own sheet, with the same column widths on every sheet. A schedule starts with a few rows; users add rows with Excel's *Insert Copied Cells* and delete unused ones, so no stray rows are left (D6, ADR 0004). | done (M3); row insertion to be checked in pilot | `test_writer.py` | +| REQ-11 | Transformer sizes are selected only from the rating family chosen for each item (`ieee-dry`, `ieee-pad` or `iec`); families are never mixed (D7). | M3 | `test_library.py` (planned) | ## Non-functional requirements @@ -31,23 +33,34 @@ it exists. | NFR-02 | The workbook is self-contained: no links to other files. | | NFR-03 | Every engineering rule has Python tests, and an automated test checks the Excel formula agrees with Python (ADR 0002). | | NFR-04 | A schedule shows what a drawing needs: at most about 7 ratings per type. Detail belongs in the specification. | +| NFR-05 | Every value in an equipment library cites a source listed in `docs/design-basis.md`. A test fails if a source is missing or unlisted. | ## Non-goals (v0.1) - Writing specifications. - A manufacturer product database. Libraries hold standard sizes and typical dimensions only. - Load calculations. Demand is an input, calculated elsewhere. +- Certifying compliance with standards. Library sizes come from the cited standards; whether a product complies is confirmed by the manufacturer's data. - DXF/PDF output, AutoCAD Data Link and native AutoCAD table objects. v0.1 gets schedules into drawings by copy-paste; the rest comes later. -## Open questions +## Decisions + +Engineering decisions behind the layout and libraries. D1–D3 are pinned by tests +(`test_grid.py`, names starting `test_d1_` etc.), so they can't change by accident; +changing one means changing its test on purpose, in a reviewed pull request. -These are engineering calls, not coding ones. Each is marked `DECIDE` in `layout.yaml`. +| # | Question | Decision | Why | +|---|---|---|---| +| D1 | Dimensions in inches or mm? | Inches. | Matches the office's existing schedules. Could become a profile setting later. | +| D2 | Show "provisional" on the schedule? | Yes: the group header reads PROVISIONAL DIMENSIONS. | Exact dimensions often aren't known at design stage; typical sizes stand in for them. | +| D3 | Generator ratings in kW or kVA? | kVA, for standby and prime ratings. | Demand served (a load) is in kVA; ratings in the same unit compare directly, with no hidden power factor. | +| D4 | Where do generator enclosure and fuel type go? | The specification, not the schedule. | They're requirements, not ratings (NFR-04). | +| D5 | Is prime rating equal to standby acceptable? | Yes, with a note to confirm on the manufacturer's datasheet. Prime above standby is an error. | Some manufacturers (e.g. CAT) rate some units with prime equal to standby. | +| D6 | How many rows per table? | As many as the user needs: one sheet per type; rows added or deleted in Excel. | No stray empty rows on drawings (ADR 0004). | +| D7 | Which transformer rating series? | Three families: IEEE dry-type (LV), IEEE liquid-filled padmount, and IEC. The selector picks only within the family chosen for each item. | Projects use US and European equipment on a 50 Hz supply; mixing series in one list would select sizes the chosen supplier doesn't make. | + +## Open questions | # | Question | Notes | |---|---|---| -| Q1 | Dimensions in inches or mm? | Sample schedules use inches. Could be a profile default. | -| Q2 | Is "provisional" shown on the schedule or in a general note? | Shown on the schedule as exact dimensions are not always known and may need be proxied | -| Q3 | Generator ratings in kW or kVA? | Demand served is load not the unit size -| Q4 | Where do generator enclosure and fuel type go? | This will be deffered to the specification document. | -| Q5 | Is prime rating equal to standby rating acceptable? | No, this does vary on manufacturer such as CAT however, as these units prime is the same as standby -| Q6 | How many data rows per table, and how are unused rows hidden? | Unlimited | +| Q8 | Do padmount transformers get their own schedule? | Padmount purchases usually state primary voltage, BIL, loop or radial feed, fluid and impedance, which the dry-type schedule doesn't have. Check what IEEE C57.12.34 asks the purchaser to specify. | \ No newline at end of file diff --git a/src/kitlist/data/library/transformer_kva.csv b/src/kitlist/data/library/transformer_kva.csv new file mode 100644 index 0000000..147a8c7 --- /dev/null +++ b/src/kitlist/data/library/transformer_kva.csv @@ -0,0 +1,65 @@ +family,phase,kva,source +ieee-dry,1,15,DOE-431.196a +ieee-dry,1,25,DOE-431.196a +ieee-dry,1,37.5,DOE-431.196a +ieee-dry,1,50,DOE-431.196a +ieee-dry,1,75,DOE-431.196a +ieee-dry,1,100,DOE-431.196a +ieee-dry,1,167,DOE-431.196a +ieee-dry,1,250,DOE-431.196a +ieee-dry,1,333,DOE-431.196a +ieee-dry,3,15,DOE-431.196a +ieee-dry,3,30,DOE-431.196a +ieee-dry,3,45,DOE-431.196a +ieee-dry,3,75,DOE-431.196a +ieee-dry,3,112.5,DOE-431.196a +ieee-dry,3,150,DOE-431.196a +ieee-dry,3,225,DOE-431.196a +ieee-dry,3,300,DOE-431.196a +ieee-dry,3,500,DOE-431.196a +ieee-dry,3,750,DOE-431.196a +ieee-dry,3,1000,DOE-431.196a +ieee-pad,1,10,DOE-431.196b +ieee-pad,1,15,DOE-431.196b +ieee-pad,1,25,DOE-431.196b +ieee-pad,1,37.5,DOE-431.196b +ieee-pad,1,50,DOE-431.196b +ieee-pad,1,75,DOE-431.196b +ieee-pad,1,100,DOE-431.196b +ieee-pad,1,167,DOE-431.196b +ieee-pad,1,250,DOE-431.196b +ieee-pad,3,15,DOE-431.196b +ieee-pad,3,30,DOE-431.196b +ieee-pad,3,45,DOE-431.196b +ieee-pad,3,75,DOE-431.196b +ieee-pad,3,112.5,DOE-431.196b +ieee-pad,3,150,DOE-431.196b +ieee-pad,3,225,DOE-431.196b +ieee-pad,3,300,DOE-431.196b +ieee-pad,3,500,DOE-431.196b +ieee-pad,3,750,DOE-431.196b +ieee-pad,3,1000,DOE-431.196b +ieee-pad,3,1500,DOE-431.196b +ieee-pad,3,2000,DOE-431.196b +ieee-pad,3,2500,DOE-431.196b +iec,3,25,IEC-60076-1 +iec,3,31.5,IEC-60076-1 +iec,3,40,IEC-60076-1 +iec,3,50,IEC-60076-1 +iec,3,63,IEC-60076-1 +iec,3,80,IEC-60076-1 +iec,3,100,IEC-60076-1 +iec,3,125,IEC-60076-1 +iec,3,160,IEC-60076-1 +iec,3,200,IEC-60076-1 +iec,3,250,IEC-60076-1 +iec,3,315,IEC-60076-1 +iec,3,400,IEC-60076-1 +iec,3,500,IEC-60076-1 +iec,3,630,IEC-60076-1 +iec,3,800,IEC-60076-1 +iec,3,1000,IEC-60076-1 +iec,3,1250,IEC-60076-1 +iec,3,1600,IEC-60076-1 +iec,3,2000,IEC-60076-1 +iec,3,2500,IEC-60076-1 \ No newline at end of file diff --git a/src/kitlist/library.py b/src/kitlist/library.py new file mode 100644 index 0000000..a652cf5 --- /dev/null +++ b/src/kitlist/library.py @@ -0,0 +1,38 @@ +"""Equipment libraries: standard sizes read from the CSV files in data/library/. + +Every row cites a source listed in docs/design-basis.md (NFR-05). +""" + +import csv +from dataclasses import dataclass +from importlib.resources import files + +FAMILIES = ("ieee-dry", "ieee-pad", "iec") # transformer rating families (D7) + + +@dataclass(frozen=True) +class TransformerRating: + """One row of transformer_kva.csv.""" + + family: str + phase: int + kva: float + source: str + + +def transformer_ratings() -> list[TransformerRating]: + """Every standard transformer rating in the library.""" + path = files("kitlist").joinpath("data/library/transformer_kva.csv") + # "utf-8-sig" also accepts the invisible marker Excel adds when it saves a CSV. + text = path.read_text(encoding="utf-8-sig") + return [ + TransformerRating(row["family"], int(row["phase"]), float(row["kva"]), row["source"]) + for row in csv.DictReader(text.splitlines()) + ] + + +def transformer_sizes(family: str, phase: int) -> list[float]: + """Standard kVA sizes for one family and phase count, smallest first.""" + if family not in FAMILIES: + raise ValueError(f"unknown transformer family '{family}'; expected one of {FAMILIES}") + return sorted(r.kva for r in transformer_ratings() if r.family == family and r.phase == phase) \ No newline at end of file diff --git a/tests/test_library.py b/tests/test_library.py new file mode 100644 index 0000000..109c159 --- /dev/null +++ b/tests/test_library.py @@ -0,0 +1,73 @@ +"""Tests for the equipment libraries (NFR-05, REQ-11).""" + +from pathlib import Path + +import pytest + +from kitlist.library import FAMILIES, transformer_ratings, transformer_sizes + +DESIGN_BASIS = Path(__file__).parents[1] / "docs" / "design-basis.md" +RATINGS = transformer_ratings() + + +def reference_ids() -> set[str]: + """The IDs in the first column of the References table in docs/design-basis.md.""" + text = DESIGN_BASIS.read_text(encoding="utf-8") + references = text.split("## References")[1].split("\n## ")[0] + ids = set() + for line in references.splitlines(): + if not line.startswith("|"): + continue + first_cell = line.split("|")[1].strip() + if first_cell != "ID" and not first_cell.startswith("---"): + ids.add(first_cell) + return ids + + +# ---- traceability (NFR-05) ---- + + +def test_reference_table_can_be_read(): + # Checks the helper above: if this fails, the other tests can't be trusted. + assert {"DOE-431.196a", "DOE-431.196b", "IEC-60076-1"} <= reference_ids() + + +def test_every_rating_cites_a_source_listed_in_the_design_basis(): + missing = {r.source for r in RATINGS} - reference_ids() + assert not missing, f"not listed in docs/design-basis.md: {sorted(missing)}" + + +# ---- families (REQ-11, D7) ---- + + +def test_every_rating_belongs_to_a_known_family(): + unknown = {r.family for r in RATINGS} - set(FAMILIES) + assert not unknown, f"unknown families: {sorted(unknown)}" + + +def test_families_are_never_mixed(): + # 25 kVA three-phase is an IEC rating, not a North American one + assert 25 in transformer_sizes("iec", phase=3) + assert 25 not in transformer_sizes("ieee-dry", phase=3) + + +def test_unknown_family_is_an_error(): + with pytest.raises(ValueError, match="unknown transformer family"): + transformer_sizes("ansi", phase=3) + + +# ---- library contents ---- + + +def test_no_rating_is_listed_twice(): + keys = [(r.family, r.phase, r.kva) for r in RATINGS] + assert len(keys) == len(set(keys)) + + +def test_sizes_come_back_smallest_first(): + sizes = transformer_sizes("ieee-dry", phase=3) + assert sizes == sorted(sizes) + + +def test_single_phase_padmounts_stay_within_c57_12_38_scope(): + assert max(transformer_sizes("ieee-pad", phase=1)) <= 250 \ No newline at end of file From 8dcbb7cffc70986a0203a5370be00be5c4b0d044 Mon Sep 17 00:00:00 2001 From: Shankar Ramharack Date: Sat, 7 Nov 2026 20:54:39 -0400 Subject: [PATCH 2/2] fixed formatting --- src/kitlist/library.py | 2 +- tests/test_library.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kitlist/library.py b/src/kitlist/library.py index a652cf5..ceecbbe 100644 --- a/src/kitlist/library.py +++ b/src/kitlist/library.py @@ -35,4 +35,4 @@ def transformer_sizes(family: str, phase: int) -> list[float]: """Standard kVA sizes for one family and phase count, smallest first.""" if family not in FAMILIES: raise ValueError(f"unknown transformer family '{family}'; expected one of {FAMILIES}") - return sorted(r.kva for r in transformer_ratings() if r.family == family and r.phase == phase) \ No newline at end of file + return sorted(r.kva for r in transformer_ratings() if r.family == family and r.phase == phase) diff --git a/tests/test_library.py b/tests/test_library.py index 109c159..a814b4b 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -70,4 +70,4 @@ def test_sizes_come_back_smallest_first(): def test_single_phase_padmounts_stay_within_c57_12_38_scope(): - assert max(transformer_sizes("ieee-pad", phase=1)) <= 250 \ No newline at end of file + assert max(transformer_sizes("ieee-pad", phase=1)) <= 250