1"""The catalogues Hardware mode draws from: discrete parts, every row with where it came from.
3Hardware mode never sizes a part continuously. It offers what can be bought or drilled, and only
4what a row here says exists. Four kinds:
6* **drills** -- number drills #40-#70 from ``engine.layerx.reconcile.NUMBER_DRILLS_IN`` (ASME
7 B94.11M, *Twist Drills*), the table the injector reconcile already drills from, plus metric drills
8 in 0.05 mm steps (``reconcile.METRIC_STEP_MM``, assumed: jobber sets in that range). Built here
9 from that table, not re-typed, so the two cannot disagree.
10* **valves** (``data/catalog/valves.json``) -- Cv options for solenoids, valves and regulators.
11* **tubes** (``data/catalog/tubes.json``) -- bore and wall pairs for lines.
12* **bottles** (``data/catalog/bottles.json``) -- pressurant bottles.
14The JSON catalogues are seeded only with the parts the team named (the Aqua 1092-50 at Cv 0.8, the
151.7 Cv press solenoid) and what the shipped drawings carry, each row naming the drawing elements
16and their own provenance. Nothing is taken from a vendor data sheet this repo does not hold
17(docs/layerx/AUDIT.md 9.10 4b: "Hardware mode must wait for cited catalogue data, or it will
18invent Cv values, bottle masses and tube ratings"). A person adds rows in
19``<userdata>/layerx/catalog/<kind>.json`` (same shape); a user row replaces a shipped row with the
24 {"id": "press-solenoid-cv1.7", "label": "...", "applies_to": ["SOL"],
25 "params": {"Cv": {"value": 1.7, "unit": "Cv"}, "bore": {"value": 6.35, "unit": "mm", "source": "manufacturer"}},
26 "source": "measured" | "manufacturer" | "estimated",
27 "provenance": "who says so, and where (data sheet, drawing element)"}
29A parameter's own ``source`` (optional) wins over the row's, for a row whose numbers do not share
30one provenance (the drawn bottle: volume measured, mass and MAWP estimated).
32``applies_to`` names drawing component types (``SOL``, ``PR``, ``KBOTTLE``) or ``line`` for an edge.
35from __future__
import annotations
39from pathlib
import Path
40from typing
import Any, Dict, List, Mapping, Optional
42SCHEMA =
"layerx.catalog/1"
43KINDS = (
"drills",
"valves",
"tubes",
"bottles")
44FILE_KINDS = (
"valves",
"tubes",
"bottles")
45SOURCES = (
"measured",
"manufacturer",
"estimated")
46SHIPPED_DIR = Path(__file__).resolve().parent /
"data" /
"catalog"
48DRILL_STANDARD =
"ASME B94.11M, Twist Drills (number sizes #40-#70)"
51def drill_rows(lo_mm: float = 0.5, hi_mm: float = 13.0) -> List[Dict[str, Any]]:
52 """Number drills from the reconcile's table and metric drills every 0.05 mm in [lo, hi]."""
55 rows = [{
"id": f
"drill-no{no}",
"label": f
"#{no} drill",
"drill": f
"#{no}",
"applies_to": [
"hole",
"orifice"],
56 "params": {
"d": {
"value": inch * 25.4,
"unit":
"mm"}},
"source":
"manufacturer",
57 "provenance": f
"{DRILL_STANDARD}: #{no} = {inch:.4f} in (engine/layerx/reconcile.py NUMBER_DRILLS_IN)"}
58 for no, inch
in NUMBER_DRILLS_IN.items()]
59 k0, k1 = int(math.ceil(lo_mm / METRIC_STEP_MM - 1e-9)), int(math.floor(hi_mm / METRIC_STEP_MM + 1e-9))
60 rows += [{
"id": f
"drill-{k * METRIC_STEP_MM:.2f}mm",
"label": f
"{k * METRIC_STEP_MM:.2f} mm drill",
61 "drill": f
"{k * METRIC_STEP_MM:.2f} mm",
"applies_to": [
"hole",
"orifice"],
62 "params": {
"d": {
"value": round(k * METRIC_STEP_MM, 4),
"unit":
"mm"}},
"source":
"estimated",
63 "provenance": (f
"assumed: metric drills in {METRIC_STEP_MM} mm steps (reconcile.METRIC_STEP_MM, stated "
64 f
"there for the small-hole range and carried here to {hi_mm:g} mm for trim plates, "
65 "which can be bored to size): check the size is stocked")}
66 for k
in range(k0, k1 + 1)]
70def _check_row(kind: str, row: Mapping[str, Any], where: str) -> Dict[str, Any]:
71 if not isinstance(row, Mapping):
72 raise ValueError(f
"{where}: a {kind} row must be an object")
73 rid = str(row.get(
"id")
or "").strip()
75 raise ValueError(f
"{where}: a {kind} row needs an id")
76 if not str(row.get(
"provenance")
or "").strip():
77 raise ValueError(f
"{where}: {kind} row {rid!r} has no provenance; a part nobody can source is not offered")
78 source = row.get(
"source")
79 if source
not in SOURCES:
80 raise ValueError(f
"{where}: {kind} row {rid!r} source {source!r}; expected one of {', '.join(SOURCES)}")
81 params = row.get(
"params")
82 if not isinstance(params, Mapping)
or not params:
83 raise ValueError(f
"{where}: {kind} row {rid!r} states no params")
84 for name, p
in params.items():
85 if not isinstance(p, Mapping)
or not isinstance(p.get(
"value"), (int, float))
or isinstance(p.get(
"value"), bool) \
86 or not math.isfinite(float(p[
"value"]))
or float(p[
"value"]) <= 0
or not str(p.get(
"unit")
or "").strip():
87 raise ValueError(f
"{where}: {kind} row {rid!r} param {name!r} needs a positive value and a unit")
88 if p.get(
"source")
is not None and p.get(
"source")
not in SOURCES:
89 raise ValueError(f
"{where}: {kind} row {rid!r} param {name!r} source {p.get('source')!r}; expected one of "
90 f
"{', '.join(SOURCES)}")
91 applies = row.get(
"applies_to")
92 if not isinstance(applies, list)
or not applies:
93 raise ValueError(f
"{where}: {kind} row {rid!r} says nothing it applies_to")
94 return {**dict(row),
"id": rid,
"origin": where}
97def _read(path: Path, kind: str) -> List[Dict[str, Any]]:
98 data = json.loads(path.read_text())
99 if not isinstance(data, Mapping)
or data.get(
"kind") != kind
or not isinstance(data.get(
"rows"), list):
100 raise ValueError(f
"{path}: not a {kind} catalogue ({SCHEMA}, kind {kind!r}, rows [...])")
101 return [
_check_row(kind, r, path.name)
for r
in data[
"rows"]]
105 return (Path(user_dir) /
"layerx" /
"catalog")
if user_dir
else None
108def load_catalogs(user_dir: Optional[Path] =
None, shipped_dir: Optional[Path] =
None) -> Dict[str, Any]:
109 """Every catalogue, shipped rows first and a user's rows over them (by id). A user file that
110 does not read is reported in ``problems``, never silently dropped, and the shipped rows stand."""
111 shipped = Path(shipped_dir)
if shipped_dir
else SHIPPED_DIR
112 out: Dict[str, Any] = {
"drills":
drill_rows(),
"problems": []}
114 for kind
in FILE_KINDS:
115 rows: Dict[str, Dict[str, Any]] = {}
116 path = shipped / f
"{kind}.json"
118 for r
in _read(path, kind):
119 rows[r[
"id"]] = {**r,
"origin":
"shipped"}
120 if udir
is not None and (udir / f
"{kind}.json").is_file():
122 for r
in _read(udir / f
"{kind}.json", kind):
123 rows[r[
"id"]] = {**r,
"origin":
"user"}
124 except (ValueError, OSError, json.JSONDecodeError)
as exc:
125 out[
"problems"].append(f
"{kind}: {exc}")
126 out[kind] = list(rows.values())
130def rows_for(catalogs: Mapping[str, Any], kind: str, applies: str) -> List[Dict[str, Any]]:
131 return [r
for r
in catalogs.get(kind)
or []
if applies
in (r.get(
"applies_to")
or [])]
134def param_si(row: Mapping[str, Any], name: str) -> Optional[float]:
135 """A row parameter in the drawing's own SI-ish units: mm for lengths, L for volume, psi, Cv."""
136 p = (row.get(
"params")
or {}).get(name)
137 if not isinstance(p, Mapping):
139 v, u = float(p[
"value"]), str(p.get(
"unit")
or "")
140 return v * {
"in": 25.4,
"m": 1e3,
"mm": 1.0}.get(u, 1.0)
if u
in (
"in",
"m",
"mm")
else v
143def neighbours(rows: List[Dict[str, Any]], key: str, current: float, n: int) -> List[Dict[str, Any]]:
144 """Up to ``n`` rows each side of ``current`` in ``key`` (nearest first, by ratio), never the
145 current size itself."""
146 scored = [(
param_si(r, key), r)
for r
in rows]
147 scored = [(v, r)
for v, r
in scored
if v
is not None and v > 0
and abs(v / current - 1.0) > 1e-6]
148 below = sorted([s
for s
in scored
if s[0] < current], key=
lambda s: current / s[0])[:n]
149 above = sorted([s
for s
in scored
if s[0] > current], key=
lambda s: s[0] / current)[:n]
150 seen, out = set(), []
151 for _, r
in below + above:
152 if r[
"id"]
not in seen:
List[Dict[str, Any]] _read(Path path, str kind)
Dict[str, Any] _check_row(str kind, Mapping[str, Any] row, str where)
List[Dict[str, Any]] rows_for(Mapping[str, Any] catalogs, str kind, str applies)
Dict[str, Any] load_catalogs(Optional[Path] user_dir=None, Optional[Path] shipped_dir=None)
List[Dict[str, Any]] neighbours(List[Dict[str, Any]] rows, str key, float current, int n)
List[Dict[str, Any]] drill_rows(float lo_mm=0.5, float hi_mm=13.0)
Optional[Path] user_catalog_dir(Optional[Path] user_dir)
Optional[float] param_si(Mapping[str, Any] row, str name)