diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90307d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +## ======== Personal ======== +.vscode/ diff --git a/README.md b/README.md index 8e439ca..52c1d94 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # LCR Connector -在3个元器件内,使用给定元器件数值列表快速找到目标数值元器件的最好拼接方式,支持电阻,电容,电感 +TODO diff --git a/legacy/.gitignore b/legacy/.gitignore new file mode 100644 index 0000000..22dcbb2 --- /dev/null +++ b/legacy/.gitignore @@ -0,0 +1,15 @@ +## ======== Personal ======== + + +## ======== Python ======== +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + diff --git a/legacy/.python-version b/legacy/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/legacy/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 0000000..1f0b4a8 --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,3 @@ +# LCR Connector (Legacy) + +在3个元器件内,使用给定元器件数值列表快速找到目标数值元器件的最好拼接方式,支持电阻,电容,电感 diff --git a/legacy/common.py b/legacy/common.py new file mode 100644 index 0000000..c7d6679 --- /dev/null +++ b/legacy/common.py @@ -0,0 +1,174 @@ +import enum +from dataclasses import dataclass + +class LcrConnException(Exception): + """The exception thrown by LCR Connector""" + pass + + +class DeviceKind(enum.IntEnum): + """The kind of device""" + + RESISTOR = enum.auto() + """Resistor device""" + CAPACITOR = enum.auto() + """Capacitor device""" + INDUCTOR = enum.auto() + """Inductor device""" + + +class JointKind(enum.IntEnum): + """The joint type between 2 devices""" + + SERIES = enum.auto() + """Series connection""" + PARALLEL = enum.auto() + """Parallel connection""" + + def flip(self) -> 'JointKind': + """ + Flip the joint kind + + Flip the joint kind from series to parallel or vice versa + + :return: The flipped joint kind + """ + match self: + case JointKind.SERIES: + return JointKind.PARALLEL + case JointKind.PARALLEL: + return JointKind.SERIES + + +@dataclass +class CircuitJoint: + """The part of circuit composed of two devices and the joint kind""" + + device_value: float + """The value of the device""" + joint_kind: JointKind + """The joint kind between this device and the next device""" + + def compute(self, value: float, device_kind: DeviceKind) -> float: + """ + Compute the joint value + + :param value: The value computed from previous devices + :param device_kind: The kind of the device + :return: The joint value computed + """ + if self.device_value <= 0 or value <= 0: + raise LcrConnException("Device value must be greater than 0") + + # We perform series connect for: series resistor, series inductor and parallel capacitor. + # We perform parallel connect for: parallel resistor, parallel inductor and series capacitor. + joint_kind = self.joint_kind + if device_kind == DeviceKind.CAPACITOR: + joint_kind = joint_kind.flip() + + match joint_kind: + case JointKind.SERIES: + return self.device_value + value + case JointKind.PARALLEL: + return (self.device_value * value) / (self.device_value + value) + + +class Circuit: + """The circuit composed of multiple joints""" + + device_value: float + """The value of the first device""" + joints: list[CircuitJoint] + """The joints between devices""" + + def __init__(self, value: float): + self.device_value = value + self.joints = [] + + def add_joint(self, joint: CircuitJoint): + self.joints.append(joint) + + def len_devices(self) -> int: + return len(self.joints) + 1 + + def compute(self, device_kind: DeviceKind) -> float: + """ + Compute the circuit value + + :param device_kind: The kind of the device + :return: The circuit value + """ + if self.device_value <= 0: + raise LcrConnException("Device value must be greater than 0") + + value = self.device_value + for joint in self.joints: + value = joint.compute(value, device_kind) + return value + + +def from_human_readable_value(strl: str) -> float: + """ + Convert human readable value to float + + :param strl: The human readable value + :return: The parsed float value + :raises ValueError: If the input string is not a valid number + """ + strl = strl.strip() + + if strl.endswith('n'): + return float(strl[0:-1]) * 1e-12 + if strl.endswith('p'): + return float(strl[0:-1]) * 1e-9 + if strl.endswith('u'): + return float(strl[0:-1]) * 1e-6 + if strl.endswith('m'): + return float(strl[0:-1]) * 1e-3 + if strl.endswith('k'): + return float(strl[0:-1]) * 1e3 + if strl.endswith('M'): + return float(strl[0:-1]) * 1e6 + if strl.endswith('G'): + return float(strl[0:-1]) * 1e9 + return float(strl) + + + +def to_human_readable_value(v: float) -> str: + """ + Convert float value to human readable value + + :param value: The float value + :return: The human readable value + """ + if v / 1e-12 < 1e3: + return "{:e} n".format(v / 1e-12) + if v / 1e-9 < 1e3: + return "{:.4f} p".format(v / 1e-9) + if v / 1e-6 < 1e3: + return "{:.4f} u".format(v / 1e-6) + if v / 1e-3 < 1e3: + return "{:.4f} m".format(v / 1e-3) + if v < 1e3: + return "{:.4f}".format(v) + if v / 1e3 < 1e3: + return "{:.4f} k".format(v / 1e3) + if v / 1e6 < 1e3: + return "{:.4f} M".format(v / 1e6) + if v / 1e9 < 1e3: + return "{:.4f} G".format(v / 1e9) + + return "{:e}".format(v) + + + + + + + + + + + + diff --git a/legacy/dataset.py b/legacy/dataset.py new file mode 100644 index 0000000..d71c6bc --- /dev/null +++ b/legacy/dataset.py @@ -0,0 +1,61 @@ +from typing import Iterable +from pathlib import Path +from dataclasses import dataclass +from .common import LcrConnException, from_human_readable_value + + +@dataclass(frozen=True) +class DataSubset: + """A list holding available device gauge values for resistor, capacitor or inductor""" + + gauges: tuple[float, ...] + """A list of available device gauge values""" + + @staticmethod + def from_iterable(str_gauges: Iterable[str]) -> "DataSubset": + # Remove redundant parts + gauges_set: set[float] = set() + for str_gauge in str_gauges: + numeric_gauge = from_human_readable_value(str_gauge) + if numeric_gauge in gauges_set: + raise LcrConnException(f"Duplicate gauge value found: {str_gauge}") + else: + gauges_set.add(numeric_gauge) + # Return value + return DataSubset(tuple(gauges_set)) + + @staticmethod + def from_file(filename: Path) -> "DataSubset": + with open(filename, "r", encoding="utf-8") as f: + legal_lines = filter(lambda line: line != "", (line.strip() for line in f)) + return DataSubset.from_iterable(legal_lines) + + +@dataclass(frozen=True) +class DataSet: + """The dataset include 3 lists of available device gauge values for resistor, capacitor and inductor""" + + resistor: DataSubset + """A list of available device gauge values for resistor""" + capacitor: DataSubset + """A list of available device gauge values for capacitor""" + inductor: DataSubset + """A list of available device gauge values for inductor""" + + @staticmethod + def from_iterable( + resistor: Iterable[str], capacitor: Iterable[str], inductor: Iterable[str] + ) -> "DataSet": + return DataSet( + DataSubset.from_iterable(resistor), + DataSubset.from_iterable(capacitor), + DataSubset.from_iterable(inductor), + ) + + @staticmethod + def from_file(resistor: Path, capacitor: Path, inductor: Path) -> "DataSet": + return DataSet( + DataSubset.from_file(resistor), + DataSubset.from_file(capacitor), + DataSubset.from_file(inductor), + ) diff --git a/legacy/lcr_connector.py b/legacy/lcr_connector.py new file mode 100644 index 0000000..fc651e5 --- /dev/null +++ b/legacy/lcr_connector.py @@ -0,0 +1,36 @@ +from .common import LcrConnException, Circuit, JointKind, to_human_readable_value + + +def _get_joint_kind_symbol(joint_kind: JointKind) -> str: + match joint_kind: + case JointKind.SERIES: + return "S" + case JointKind.PARALLEL: + return "P" + + +def _illustrate_circuit(circuit: Circuit) -> None: + match circuit.len_devices(): + case 1: + dev1 = to_human_readable_value(circuit.device_value) + print(f"{dev1}") + case 2: + dev1 = to_human_readable_value(circuit.device_value) + joint1 = circuit.joints[0] + j1 = _get_joint_kind_symbol(joint1.joint_kind) + dev2 = to_human_readable_value(joint1.device_value) + print(f"[{j1}] ┬ {dev1}") + print(f" └ {dev2}") + case 3: + dev1 = to_human_readable_value(circuit.device_value) + joint1 = circuit.joints[0] + j1 = _get_joint_kind_symbol(joint1.joint_kind) + dev2 = to_human_readable_value(joint1.device_value) + joint2 = circuit.joints[1] + j2 = _get_joint_kind_symbol(joint2.joint_kind) + dev3 = to_human_readable_value(joint2.device_value) + print(f"[{j2}] ┬ [{j1}] ┬ {dev1}") + print(f" │ └ {dev2}") + print(f" └ {dev3}") + case _: + raise LcrConnException("Circuit too complex to illustrate") diff --git a/main.py b/legacy/main.py similarity index 100% rename from main.py rename to legacy/main.py diff --git a/legacy/pyproject.toml b/legacy/pyproject.toml new file mode 100644 index 0000000..f9cf5e2 --- /dev/null +++ b/legacy/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "lcr-connector" +version = "0.1.0" +description = "Use as much 3 devices to reach target value for resistor, capacitor and inductor." +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] diff --git a/legacy/resolver/__init__.py b/legacy/resolver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/legacy/resolver/astar.py b/legacy/resolver/astar.py new file mode 100644 index 0000000..d801962 --- /dev/null +++ b/legacy/resolver/astar.py @@ -0,0 +1,15 @@ +from typing import Iterator +from .common import Resolver, ResolverRequest, ResolverResult, ResultPriority +from ..dataset import DataSet + +class AStarResolver(Resolver): + """ + A resolver that uses A* algorithm to find the best matching circuit. + """ + + def __init__(self, dataset: DataSet): + pass + + + def resolve(self, request: ResolverRequest) -> Iterator[ResolverResult]: + pass diff --git a/legacy/resolver/common.py b/legacy/resolver/common.py new file mode 100644 index 0000000..338acd0 --- /dev/null +++ b/legacy/resolver/common.py @@ -0,0 +1,102 @@ +import enum +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Iterator +from ..common import DeviceKind, Circuit + + +class ResultPriority(enum.Enum): + """ + The priority of the result. + """ + + LESS_DEVICES = enum.auto() + """Less devices is the first priority.""" + MORE_ACCURACY = enum.auto() + """More accuracy is the first priority.""" + + +@dataclass +class ResolverRequest: + """ + The request object for the resolver. + """ + + device_kind: DeviceKind + """The kind of device to resolve.""" + target_value: float + """The target value of the device.""" + tolerance: float + """The tolerance of the device.""" + result_priority: ResultPriority + """The priority of the result.""" + count_limit: int + """The limit of the count of results.""" + + +class ResolverResult: + """ + The result of the resolver. + """ + + circuit: Circuit + """The circuit of the result.""" + + __value_cache: float | None + """The cache of the circuit value.""" + __difference_cache: float | None + """The cache of the difference between the target value and the circuit value.""" + __relative_difference_cache: float | None + """The cache of the relative difference between the target value and the circuit value.""" + + def __init__(self, circuit: Circuit): + self.circuit = circuit + self.__value_cache = None + self.__difference_cache = None + self.__relative_difference_cache = None + + def compute(self, device_kind: DeviceKind) -> float: + """ + Compute the circuit value. + """ + if self.__value_cache is None: + self.__value_cache = self.circuit.compute(device_kind) + return self.__value_cache + + def difference(self, target_value: float, device_kind: DeviceKind) -> float: + """ + Get the difference between the target value and the circuit value. + """ + if self.__difference_cache is None: + self.__difference_cache = abs( + target_value - self.circuit.compute(device_kind) + ) + return self.__difference_cache + + def relative_difference( + self, target_value: float, device_kind: DeviceKind + ) -> float: + """ + Get the relative difference between the target value and the circuit value. + """ + if self.__relative_difference_cache is None: + self.__relative_difference_cache = ( + abs(target_value - self.circuit.compute(device_kind)) / target_value + ) + return self.__relative_difference_cache + + def len_devices(self) -> int: + """ + Get the number of devices in the circuit. + """ + return self.circuit.len_devices() + + +class Resolver(ABC): + """ + Abstract base class for all resolvers. + """ + + @abstractmethod + def resolve(self, request: ResolverRequest) -> Iterator[ResolverResult]: + pass diff --git a/legacy/resolver/lut.py b/legacy/resolver/lut.py new file mode 100644 index 0000000..010e5d9 --- /dev/null +++ b/legacy/resolver/lut.py @@ -0,0 +1,109 @@ +import struct +from typing import Iterator, BinaryIO +from pathlib import Path +from .common import Resolver, ResolverRequest, ResolverResult, ResultPriority +from ..dataset import DataSet +from ..common import Circuit, CircuitJoint, JointKind, LcrConnException + +class LutResolver(Resolver): + """ + A resolver that uses a lookup table to find the best matching circuit. + """ + + lut: tuple[Circuit] + + def __init__(self, lut: tuple[Circuit]): + self.lut = lut + + @staticmethod + def from_dataset(dataset: DataSet) -> 'LutResolver': + pass + + @staticmethod + def from_cache(filename: Path) -> 'LutResolver': + with open(filename, "rb") as f: + cnt = _read_int(f) + return LutResolver(tuple(LutItem.from_cache(f) for _ in range(cnt))) + + def save_as_cache(self, filename: Path) -> None: + with open(filename, "wb") as f: + _write_int(f, len(self.lut)) + for item in self.lut: + item.save_as_cache(f) + + def resolve(self, request: ResolverRequest) -> Iterator[ResolverResult]: + pass + + +class LutItem: + """ + An item in the lookup table. + """ + + circuit: Circuit + """The circuit represented by this item.""" + __value_cache: float | None + """The cached computed value of the circuit, or None if it has not been cached yet.""" + + def __init__(self, circuit: Circuit): + self.circuit = circuit + + @staticmethod + def from_cache(f: BinaryIO) -> 'LutItem': + cnt = _read_int(f) + + if cnt < 1: + raise LcrConnException("Invalid circuit count in LUT item") + device_value = _read_double(f) + circuit = Circuit(device_value) + cnt -= 1 + + for _ in range(cnt): + j = JointKind.SERIES if _read_bool(f) else JointKind.PARALLEL + dev = _read_double(f) + joint = CircuitJoint(j, dev) + circuit.add_joint(joint) + + return LutItem(circuit) + + def save_as_cache(self, f: BinaryIO) -> None: + _write_int(f, self.circuit.len_devices()) + _write_double(f, self.circuit.device_value) + for joint in self.circuit.joints(): + _write_bool(f, joint.kind == JointKind.SERIES) + _write_double(f, joint.value) + + def compute(self) -> float: + """The computed value of the circuit.""" + if self.__value_cache is None: + self.__value_cache = self.circuit.value() + return self.__value_cache + + + +DOUBLE_PACKER = struct.Struct("d") +INT_PACKER = struct.Struct("I") +BOOL_PACKER = struct.Struct("?") + +def _read_double(fs) -> float: + return DOUBLE_PACKER.unpack(fs.read(DOUBLE_PACKER.size))[0] + +def _read_int(fs) -> int: + return INT_PACKER.unpack(fs.read(INT_PACKER.size))[0] + +def _read_bool(fs) -> bool: + return BOOL_PACKER.unpack(fs.read(BOOL_PACKER.size))[0] + +def _write_double(fs, num: float): + fs.write(DOUBLE_PACKER.pack(num)) + +def _write_int(fs, num: int): + fs.write(INT_PACKER.pack(num)) + +def _write_bool(fs, num: bool): + fs.write(BOOL_PACKER.pack(num)) + + + + + diff --git a/legacy/uv.lock b/legacy/uv.lock new file mode 100644 index 0000000..c774378 --- /dev/null +++ b/legacy/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "lcr-connector" +version = "0.1.0" +source = { virtual = "." }