Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
97c6f2bbeThis file is pinned to the selected release. Download the original to open it in your design tool.
"""`akcli calc` — standards-cited engineering calculators.
``calc list`` / ``calc info <name>`` / ``calc <name> key=value ...`` / ``calc
batch <file>``, with ``--md`` (markdown table), ``--json`` (compute envelope),
and ``--ops`` (emit an op-list — macro or place_component — from a design
calculator). Heavy
imports stay LAZY per handler.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from ..errors import EXIT
from ._shared import _did_you_mean, _dumps, _emit, _ExitWith, _stamp
def _calc_md(doc: dict) -> str:
"""Render one compute() envelope as a markdown table."""
from ..calc.si import fmt_eng
lines = [f"### {doc['title']}", "",
"| result | value | note |", "|---|---|---|"]
for k, cell in doc["results"].items():
v, unit = cell["value"], cell.get("unit", "")
if isinstance(v, float):
plain = unit not in ("Ω", "V", "A", "W", "F", "H", "Hz", "s", "m")
shown = f"{v:.6g} {unit}".strip() if plain else fmt_eng(v, unit)
elif isinstance(v, list):
shown = "; ".join(str(x) for x in v) if v and not isinstance(v[0], dict) \
else f"{len(v)} entries (use --json)"
else:
shown = f"{v} {unit}".strip()
lines.append(f"| {k} | {shown} | {cell.get('note', '')} |")
lines += ["", f"*Reference: {doc['reference']}*"]
return "\n".join(lines)
def _calc_batch(args: argparse.Namespace, params: list[str]) -> int:
"""`calc batch <file|->`: run a JSON job list, emit an array of envelopes."""
import json as _json
import sys as _sys
from .. import calc as calcmod
from ..calc.registry import CALCS, CalcError
if not params:
raise _ExitWith(EXIT["USAGE"], "ERROR: calc batch needs a jobs file ('-' = stdin)")
src = _sys.stdin.read() if params[0] == "-" else None
if src is None:
path = Path(params[0])
if not path.exists():
raise _ExitWith(EXIT["NOT_FOUND"], f"ERROR: {path} not found")
src = path.read_text(encoding="utf-8")
try:
doc = _json.loads(src)
jobs = doc["jobs"] if isinstance(doc, dict) else doc
assert isinstance(jobs, list)
except Exception:
raise _ExitWith(EXIT["USAGE"],
'ERROR: batch input must be {"jobs": [{"calc": ..., "params": {...}}, ...]}')
out, failed = [], 0
for i, job in enumerate(jobs):
name = job.get("calc") if isinstance(job, dict) else None
raw = {k: str(v) for k, v in (job.get("params") or {}).items()} \
if isinstance(job, dict) else {}
if not name or name not in CALCS:
out.append({"calc": name, "error": f"job {i}: unknown calculator {name!r}"})
failed += 1
continue
try:
out.append(calcmod.compute(name, raw))
except CalcError as exc:
out.append({"calc": name, "error": str(exc)})
failed += 1
_emit(_dumps(out))
if failed:
print(f"{failed}/{len(jobs)} jobs failed", file=_sys.stderr)
return EXIT["FINDINGS"] if failed else EXIT["OK"]
def _cmd_calc(args: argparse.Namespace) -> int:
"""`calc list` / `calc info <name>` / `calc <name> key=value ...`."""
from .. import calc as calcmod
from ..calc.registry import CALCS, CalcError
from ..calc.si import fmt_eng
name = getattr(args, "name", None)
params = list(getattr(args, "params", []) or [])
if not name or name == "list":
if getattr(args, "json", False):
table = {
c.name: {
"title": c.title, "group": c.group,
"params": [{"name": p.name, "unit": p.unit, "help": p.help,
"required": p.default is None,
**({"choices": list(p.choices)} if p.choices else {})}
for p in c.params],
"reference": c.reference,
**({"notes": c.notes} if c.notes else {}),
}
for c in sorted(CALCS.values(), key=lambda c: (c.group, c.name))
}
_emit(_dumps(table))
return EXIT["OK"]
lines, group = [], None
for c in sorted(CALCS.values(), key=lambda c: (c.group, c.name)):
if c.group != group:
group = c.group
lines.append(f"[{group}]")
req = " ".join(p.name for p in c.params if p.default is None)
lines.append(f" {c.name:18} {c.title}" + (f" ({req})" if req else ""))
lines.append("`akcli calc info <name>` shows params + the reference; "
"`akcli calc <name> key=value ...` runs it.")
_emit("\n".join(lines))
return EXIT["OK"]
if name == "info":
target = params[0] if params else None
c = CALCS.get(target or "")
if c is None:
raise _ExitWith(EXIT["USAGE"],
f"ERROR: unknown calculator {target!r}"
f"{_did_you_mean(target or '', CALCS)} "
"(see `akcli calc list`)")
if getattr(args, "json", False):
# Same shape as one `calc list` entry (plus its own `name`), so an
# agent parses both with one reader.
_emit(_dumps(_stamp({
"name": c.name, "title": c.title, "group": c.group,
"params": [{"name": p.name, "unit": p.unit, "help": p.help,
"required": p.default is None,
**({"default": p.default} if p.default is not None else {}),
**({"choices": list(p.choices)} if p.choices else {})}
for p in c.params],
"reference": c.reference,
**({"notes": c.notes} if c.notes else {}),
})))
return EXIT["OK"]
lines = [f"{c.name} — {c.title}", ""]
for p in c.params:
d = "required" if p.default is None else f"default {p.default}"
ch = f" one of {list(p.choices)}" if p.choices else ""
lines.append(f" {p.name:14} [{p.unit or '-'}] {p.help} ({d}){ch}")
lines += ["", f"Reference: {c.reference}"]
if c.notes:
lines.append(f"Note: {c.notes}")
_emit("\n".join(lines))
return EXIT["OK"]
if name == "batch":
return _calc_batch(args, params)
if name not in CALCS:
raise _ExitWith(EXIT["USAGE"],
f"ERROR: unknown calculator {name!r}"
f"{_did_you_mean(name, CALCS)} "
"(see `akcli calc list`)")
raw: dict[str, str] = {}
for tok in params:
if "=" not in tok:
raise _ExitWith(EXIT["USAGE"],
f"ERROR: expected key=value, got {tok!r}")
k, v = tok.split("=", 1)
raw[k.strip()] = v.strip()
try:
doc = calcmod.compute(name, raw)
except CalcError as exc:
raise _ExitWith(EXIT["USAGE"], f"ERROR: {exc}")
if getattr(args, "ops", None):
from ..calc import opsmap
try:
opdoc = opsmap.to_oplist(name, doc)
except CalcError as exc:
raise _ExitWith(EXIT["USAGE"], f"ERROR: {exc}")
rendered = _dumps(opdoc)
if args.ops == "-":
_emit(rendered)
return EXIT["OK"]
Path(args.ops).write_text(rendered + "\n", encoding="utf-8", newline="\n")
import sys as _sys
print(f"op-list written to {args.ops} "
f"({len(opdoc['ops'])} ops — edit coordinates, then `akcli plan`)",
file=_sys.stderr)
if getattr(args, "md", False):
_emit(_calc_md(doc))
return EXIT["OK"]
if getattr(args, "json", False):
_emit(_dumps(_stamp(doc)))
return EXIT["OK"]
lines = [f"{doc['title']}"]
for key, cell in doc["results"].items():
val, unit = cell["value"], cell.get("unit", "")
if isinstance(val, float):
# SI prefixes scale linearly — only prefix bare base units
# (never mm, °C/W, m², Ω/km, ...)
prefixable = unit in ("Ω", "V", "A", "W", "F", "H", "Hz", "s", "m")
shown = (fmt_eng(val, unit) if prefixable
else f"{val:.6g} {unit}".strip())
elif isinstance(val, list):
shown = f"{len(val)} entries (use --json for detail)"
else:
shown = f"{val} {unit}".strip()
note = f" ({cell['note']})" if cell.get("note") else ""
lines.append(f" {key:22} {shown}{note}")
lines.append(f"reference: {doc['reference']}")
_emit("\n".join(lines))
return EXIT["OK"]
class _CalcNameChoices:
"""Lazy ``choices`` for the ``calc`` NAME positional.
Gives argparse-NATIVE validation (an unknown name fails at parse time
with the full ``choose from`` list — and, under ``--json``, the standard
USAGE error envelope) while the calculator registry stays unimported
until a ``calc`` invocation actually parses a name. Everything else
(``akcli --help``, every other command) keeps zero calc import cost.
"""
_VERBS = ("list", "info", "batch")
def __contains__(self, name: object) -> bool:
if name in self._VERBS:
return True
from ..calc.registry import CALCS
return name in CALCS
def __iter__(self):
from ..calc.registry import CALCS
return iter([*self._VERBS, *sorted(CALCS)])
def register(sub, common) -> None:
p = sub.add_parser("calc", parents=[common],
help="engineering calculators (E-series, IPC-2221, via, "
"SMPS, 555, I2C, ... — every result cites its source)",
epilog=("subcommands:\n"
" list every calculator, grouped "
"(--json: machine-readable table)\n"
" info NAME one calculator's params, "
"defaults + reference\n"
" batch FILE|- run a JSON job list "
'({"jobs": [{"calc", "params"}, ...]})\n'
" NAME key=value run one calculator"),
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("name", nargs="?", metavar="NAME",
choices=_CalcNameChoices(),
help="calculator name, or `list` / `info <name>` / "
"`batch <file>` (unknown names fail at parse time "
"with the full list)")
p.add_argument("params", nargs="*", metavar="key=value",
help="inputs, engineering notation ok (4k7, 100n, 35u)")
p.add_argument("--md", action="store_true",
help="render the result as a markdown table")
p.add_argument("--ops", metavar="FILE",
help="also emit a place_component op-list with the computed "
"values ('-' = stdout; design-type calculators only)")
p.set_defaults(handler=_cmd_calc)