Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
"""IEC 60063 preferred-number (E-) series + resistor combination search.
E1/E3/E6/E12/E24 are the **tabulated** historical series from IEC 60063:2015
(they do NOT follow the geometric formula — e.g. E24 has 2.7, 3.0, 3.3 where
the formula gives 2.61, 2.87, 3.16). E48/E96/E192 are formula series:
``round(10^(i/N), 3 significant digits)`` with the single standardized
exception 9.20 (calculated 9.19) in E192.
The combination search mirrors KiCad's resistor calculator behavior: build a
target resistance from up to four standard values (series/parallel), with the
exact requested value and up to two user-excluded values removed from the
pool (component-availability workaround).
References:
* IEC 60063:2015, *Preferred number series for resistors and capacitors*.
* Combination-search behavior cross-checked against KiCad ``pcb_calculator``
(independent reimplementation; no KiCad code used).
"""
from __future__ import annotations
import bisect
from typing import cast
from .registry import CalcError, Param, Result, register
from .si import fmt_eng
__all__ = ["SERIES", "series_values", "nearest", "snap"]
# --- IEC 60063:2015 tabulated series (must NOT be generated by formula) ---- #
_E1 = (1.0,)
_E3 = (1.0, 2.2, 4.7)
_E6 = (1.0, 1.5, 2.2, 3.3, 4.7, 6.8)
_E12 = (1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2)
_E24 = (1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0,
3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1)
def _formula(n: int) -> tuple[float, ...]:
"""E48/E96/E192 per IEC 60063: 10^(i/n) to 3 significant digits."""
vals = []
for i in range(n):
v = round(10 ** (i / n), 2)
# standardized exception: IEC 60063 lists 9.20 where 10^(185/192)
# rounds to 9.19
if n == 192 and abs(v - 9.19) < 1e-9:
v = 9.20
vals.append(v)
return tuple(vals)
SERIES: dict[str, tuple[float, ...]] = {
"E1": _E1, "E3": _E3, "E6": _E6, "E12": _E12, "E24": _E24,
"E48": _formula(48), "E96": _formula(96), "E192": _formula(192),
}
_SERIES_CHOICES = tuple(SERIES)
def series_values(series: str, lo: float, hi: float) -> list[float]:
"""All ``series`` values in ``[lo, hi]``, spanning decades."""
base = SERIES[series]
out = []
dec = 1e-12
while dec <= hi:
for m in base:
v = m * dec
if lo <= v <= hi:
out.append(round(v, 12))
dec *= 10
return out
def nearest(value: float, series: str) -> float:
"""Nearest ``series`` value to ``value`` (any decade)."""
if value <= 0:
raise CalcError("value must be positive")
base = SERIES[series]
best, err = base[0], float("inf")
import math
exp = math.floor(math.log10(value))
for d in (exp - 1, exp, exp + 1):
for m in base:
v = m * 10 ** d
e = abs(v - value)
if e < err:
best, err = v, e
return round(best, 12)
def snap(value: float, series: str) -> tuple[float, float]:
"""(nearest value, signed error %)."""
v = nearest(value, series)
return v, (v - value) / value * 100.0
# --------------------------------------------------------------------------- #
# calculators
# --------------------------------------------------------------------------- #
@register(
"eseries", "E-series preferred value (snap + neighbors)", "passives",
"IEC 60063:2015, Preferred number series for resistors and capacitors",
(Param("value", "Ω/F/H", "target value to snap"),
Param("series", "", "E series", default="E24", choices=_SERIES_CHOICES)),
notes="E1..E24 are the tabulated historical values; E48/E96/E192 follow "
"10^(i/N) rounded to 3 significant digits (E192 exception: 9.20).",
)
def _calc_eseries(value: float, series: str) -> list[Result]:
vals = series_values(series, value / 12.0, value * 12.0)
below = max((v for v in vals if v <= value), default=None)
above = min((v for v in vals if v >= value), default=None)
near, err = snap(value, series)
out = [
Result("nearest", near, "", f"{err:+.3f}% vs target"),
Result("nearest_str", fmt_eng(near)),
]
if below is not None:
out.append(Result("floor", below))
if above is not None:
out.append(Result("ceil", above))
return out
def _pairs(values: list[float]) -> list[tuple[float, str]]:
"""All 2-element series/parallel combos, sorted by value."""
combos: list[tuple[float, str]] = []
n = len(values)
for i in range(n):
a = values[i]
for j in range(i, n):
b = values[j]
combos.append((a + b, f"{fmt_eng(a)} + {fmt_eng(b)}"))
combos.append((a * b / (a + b), f"{fmt_eng(a)} ∥ {fmt_eng(b)}"))
combos.sort(key=lambda t: t[0])
return combos
def _best_near(sorted_combos: list[tuple[float, str]],
target: float) -> tuple[float, str] | None:
"""Best (value, expr) in a sorted combo list near ``target``."""
keys = [c[0] for c in sorted_combos]
i = bisect.bisect_left(keys, target)
best: tuple[float, str] | None = None
for k in (i - 1, i, i + 1):
if 0 <= k < len(sorted_combos):
v, expr = sorted_combos[k]
if best is None or abs(v - target) < abs(best[0] - target):
best = (v, expr)
return best
@register(
"rcombo", "Resistor combination search (2–4 resistors)", "passives",
"IEC 60063:2015 value pool; search behavior per KiCad pcb_calculator "
"resistor calculator (independent reimplementation)",
(Param("target", "Ω", "resistance to synthesize"),
Param("series", "", "E series pool", default="E24", choices=_SERIES_CHOICES),
Param("exclude1", "Ω", "excluded value (availability)", default=0.0),
Param("exclude2", "Ω", "second excluded value", default=0.0),
Param("rmin", "Ω", "pool lower bound", default=10.0),
Param("rmax", "Ω", "pool upper bound", default=1e6)),
notes="The exact target value is always excluded from the pool. Search "
"space: 2R exhaustive; 3R = single ⊕ pair; 4R = pair ⊕ pair "
"(series or parallel at each ⊕).",
)
def _calc_rcombo(target: float, series: str, exclude1: float, exclude2: float,
rmin: float, rmax: float) -> list[Result]:
if target <= 0:
raise CalcError("target must be positive")
excl = {round(v, 9) for v in (target, exclude1, exclude2) if v > 0}
pool = [v for v in series_values(series, rmin, rmax)
if round(v, 9) not in excl]
if not pool:
raise CalcError("empty value pool (check rmin/rmax/excludes)")
def sol(v: float, expr: str, count: int) -> dict[str, object]:
return {"expression": expr, "value": v, "n": count,
"error_pct": round((v - target) / target * 100.0, 6)}
# 1R
best_single: float | None = None
err = float("inf")
for v in pool:
if abs(v - target) < err:
best_single, err = v, abs(v - target)
assert best_single is not None # pool is non-empty (checked above)
one = sol(best_single, fmt_eng(best_single), 1)
# 2R exhaustive
pairs = _pairs(pool)
v2 = _best_near(pairs, target)
assert v2 is not None # pairs is non-empty (pool is non-empty)
two = sol(v2[0], v2[1], 2)
# 3R: single ⊕ pair
three = None
for a in pool:
# series: pair must hit target - a
if target - a > 0:
hit = _best_near(pairs, target - a)
if hit:
v = a + hit[0]
if three is None or abs(v - target) < abs(cast(float, three["value"]) - target):
three = sol(v, f"{fmt_eng(a)} + ({hit[1]})", 3)
# parallel: pair must hit a*target/(a-target)
if a > target:
want = a * target / (a - target)
hit = _best_near(pairs, want)
if hit:
v = a * hit[0] / (a + hit[0])
if three is None or abs(v - target) < abs(cast(float, three["value"]) - target):
three = sol(v, f"{fmt_eng(a)} ∥ ({hit[1]})", 3)
# 4R: pair ⊕ pair
four = None
for p1, e1 in pairs:
if target - p1 > 0:
hit = _best_near(pairs, target - p1)
if hit:
v = p1 + hit[0]
if four is None or abs(v - target) < abs(cast(float, four["value"]) - target):
four = sol(v, f"({e1}) + ({hit[1]})", 4)
if p1 > target:
want = p1 * target / (p1 - target)
hit = _best_near(pairs, want)
if hit:
v = p1 * hit[0] / (p1 + hit[0])
if four is None or abs(v - target) < abs(cast(float, four["value"]) - target):
four = sol(v, f"({e1}) ∥ ({hit[1]})", 4)
solutions = [s for s in (one, two, three, four) if s]
best = min(solutions, key=lambda s: abs(cast(float, s["error_pct"])))
return [Result("best", best["expression"], "",
f"= {fmt_eng(cast(float, best['value']), 'Ω')} "
f"({cast(float, best['error_pct']):+.4f}%)"),
Result("solutions", solutions)]
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.