import bisect from itertools import chain, product from functools import cached_property from .common import Resolver from .bfs import BfsResolver from ..dataset import DatasetCollection, Dataset from ..common import Circuit, DeviceKind, JointKind, CircuitValueTrait from ..query import Request, Response class LutItem: """ An item in the lookup table. """ __circuit: Circuit """The circuit represented by this item.""" __value: float """The value of this circuit.""" def __init__(self, circuit: Circuit, device_kind: DeviceKind): self.__circuit = circuit self.__value = self.__circuit.compute(device_kind) @property def circuit(self) -> Circuit: return self.__circuit @property def value(self) -> float: return self.__value class LutResolver(Resolver): """ A resolver that uses a lookup table to find the best matching circuit. """ __resistor_lut: list[LutItem] """The lookup table for resistors.""" __capacitor_lut: list[LutItem] """The lookup table for capacitors.""" __inductor_lut: list[LutItem] """The lookup table for inductors.""" def __init__(self, datasets: DatasetCollection): self.__resistor_lut = self.__build_lut( datasets.resistor_values, DeviceKind.RESISTOR ) self.__capacitor_lut = self.__build_lut( datasets.capacitor_values, DeviceKind.CAPACITOR ) self.__inductor_lut = self.__build_lut( datasets.inductor_values, DeviceKind.INDUCTOR ) def __build_lut(self, dataset: Dataset, device_kind: DeviceKind) -> list[LutItem]: lut = [ LutItem(circuit, device_kind) for circuit in chain( BfsResolver.iter_one_device_circuit(dataset), BfsResolver.iter_two_devices_circuit(dataset), BfsResolver.iter_three_devices_circuit(dataset), ) ] lut.sort(key=lambda item: item.value) return lut def resolve(self, request: Request) -> Response: lut: list[LutItem] match request.device_kind: case DeviceKind.RESISTOR: lut = self.__resistor_lut case DeviceKind.CAPACITOR: lut = self.__capacitor_lut case DeviceKind.INDUCTOR: lut = self.__inductor_lut target = request.target_value count_limit = request.count_limit bucket: list[Circuit] = [] # Locate the insertion point of target in the sorted LUT. # left/right start at the two nearest neighbours and expand outward. idx = bisect.bisect_left(lut, target, key=lambda item: item.value) left = idx - 1 right = idx # Expand outward non-symmetrically: at each step compare the two # candidates on each side and advance the one that is closer to the # target. This guarantees items are visited in strictly increasing # difference order, so the first N items within tolerance are exactly # the N best matches. while left >= 0 or right < len(lut): if len(bucket) >= count_limit: break if left < 0: go_left = False elif right >= len(lut): go_left = True else: go_left = (target - lut[left].value) <= (lut[right].value - target) if go_left: item = lut[left] left -= 1 else: item = lut[right] right += 1 diff = abs(target - item.value) # Since the LUT is sorted, values on each side only move further # from target as we advance. Once one side exceeds tolerance, # the rest of that side is guaranteed out of range — disable it. if diff > request.tolerance: if go_left: left = -1 else: right = len(lut) continue bucket.append(item.circuit) return Response(request, bucket)