1
0

feat: remove astar, use bfs instead

This commit is contained in:
2026-06-25 12:31:09 +08:00
parent 625628cc1a
commit 7fa0f56495
7 changed files with 406 additions and 166 deletions

View File

@@ -10,7 +10,7 @@ from .dataset import (
from_human_readable_value,
)
from .query import Request, ResponsePriority, Response
from .resolver import Resolver, LutResolver, AStarResolver
from .resolver import Resolver, LutResolver, BfsResolver
_TStrEnum = TypeVar("_TStrEnum", bound=enum.StrEnum)
@@ -22,8 +22,8 @@ class AppResolver(enum.StrEnum):
LUT = "lut"
"""The look-up table resolver."""
ASTAR = "astar"
"""The A* resolver."""
BFS = "bfs"
"""The BFS resolver."""
@dataclass
@@ -65,8 +65,8 @@ class App:
match self.__config.resolver:
case AppResolver.LUT:
self.__resolver = LutResolver(self.__dataset)
case AppResolver.ASTAR:
self.__resolver = AStarResolver(self.__dataset)
case AppResolver.BFS:
self.__resolver = BfsResolver(self.__dataset)
def run(self) -> None:
"""

View File

@@ -1,4 +1,5 @@
import enum
from typing import Optional
class LcrConnException(Exception):
@@ -267,3 +268,107 @@ class Circuit:
return self.__third_device_subckt.device_value
else:
raise LcrConnException("No third device")
class CircuitValueTrait:
"""The trait for handful circuit computation"""
__device_kind: DeviceKind
"""The kind of the device"""
__target_value: float
"""The target value"""
def __init__(self, device_kind: DeviceKind, target_value: float) -> None:
self.__device_kind = device_kind
self.__target_value = target_value
def value(self, circuit: Circuit) -> float:
"""
The value of this circuit.
:param circuit: The circuit for computation.
:return: The value.
"""
return circuit.compute(self.__device_kind)
def difference(self, circuit: Circuit, value: Optional[float] = None) -> float:
"""
The signed difference between the target value and the value of this circuit.
Positive value indicates that the value of this circuit is greater than the target value.
Negative value indicates that the value of this circuit is less than the target value.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:return: The signed difference.
"""
if value is None:
value = self.value(circuit)
return value - self.__target_value
def unsigned_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:return: The unsigned difference.
"""
if difference is None:
difference = self.difference(circuit, value)
return abs(difference)
def relative_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
) -> float:
"""
The signed relative difference between the target value and the value of this circuit.
Positive value indicates that the value of this circuit is greater than the target value.
Negative value indicates that the value of this circuit is less than the target value.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:return: The signed relative difference.
"""
if difference is None:
difference = self.difference(circuit, value)
return difference / self.__target_value
def unsigned_relative_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
relative_difference: Optional[float] = None,
) -> float:
"""
The unsigned relative difference between the target value and the value of this circuit.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:param relative_difference: The relative difference of the circuit computed by the `relative_difference` method
for reducing computation steps, or None if you request this method to compute the relative difference.
:return: The unsigned relative difference.
"""
if relative_difference is None:
relative_difference = self.relative_difference(circuit, value, difference)
return abs(relative_difference)

View File

@@ -1,9 +1,8 @@
import enum
import struct
from functools import cached_property
from dataclasses import dataclass
from typing import Iterable, Iterator
from .common import DeviceKind, Circuit, CircuitDeviceScale
from .common import DeviceKind, Circuit, CircuitValueTrait
class ResponsePriority(enum.Enum):
@@ -35,92 +34,6 @@ class Request:
"""The limited count of results."""
class ResponseDeduperItem:
"""
The item for response deduplicator.
"""
__circuit: Circuit
"""The circuit of this deduplicator item."""
def __init__(self, circuit: Circuit) -> None:
self.__circuit = circuit
__ONE_PACKER = struct.Struct("=id")
__TWO_PACKER = struct.Struct("=iidd")
__THREE_PACKER = struct.Struct("=iiiddd")
@cached_property
def __uniform_circuit_presentation(self) -> bytes:
c = self.__circuit
match c.device_scale:
case CircuitDeviceScale.ONE:
return self.__ONE_PACKER.pack(1, c.first_device_value)
case CircuitDeviceScale.TWO:
v1, v2 = sorted([c.first_device_value, c.second_device_value])
return self.__TWO_PACKER.pack(2, int(c.second_device_joint), v1, v2)
case CircuitDeviceScale.THREE:
v1, v2, v3 = (
c.first_device_value,
c.second_device_value,
c.third_device_value,
)
j2, j3 = int(c.second_device_joint), int(c.third_device_joint)
if j2 == j3:
v1, v2, v3 = sorted([v1, v2, v3])
else:
v1, v2 = sorted([v1, v2])
return self.__THREE_PACKER.pack(3, j2, j3, v1, v2, v3)
@cached_property
def __uniform_circuit_hash(self) -> int:
return hash(self.__uniform_circuit_presentation)
def __eq__(self, other: object) -> bool:
if not isinstance(other, ResponseDeduperItem):
return False
return (
self.__uniform_circuit_presentation == other.__uniform_circuit_presentation
)
def __hash__(self) -> int:
return self.__uniform_circuit_hash
@property
def circuit(self) -> Circuit:
"""
The circuit of this response item.
:return: The circuit.
"""
return self.__circuit
class ResponseDeduper:
"""
The deduplicator for response circuits to deduplicate equivalent circuits.
"""
__circuits: set[ResponseDeduperItem]
def __init__(self) -> None:
self.__circuits = set()
def add(self, circuit: Circuit) -> None:
self.__circuits.add(ResponseDeduperItem(circuit))
def __len__(self) -> int:
return len(self.__circuits)
def __iter__(self) -> Iterator[Circuit]:
return map(lambda x: x.circuit, self.__circuits)
class ResponseItem:
"""
The possible solution given by the resolver.
@@ -128,20 +41,12 @@ class ResponseItem:
__circuit: Circuit
"""The circuit of the response item."""
__value: float
"""The value of the response circuit."""
__difference: float
"""The signed difference between the target value and the value of this circuit."""
__relative_difference: float
"""The signed relative difference between the target value and the value of this circuit."""
__cv_trait: CircuitValueTrait
"""The trait for computing circuit values."""
def __init__(
self, circuit: Circuit, device_kind: DeviceKind, target_value: float
) -> None:
def __init__(self, circuit: Circuit, cv_trait: CircuitValueTrait) -> None:
self.__circuit = circuit
self.__value = self.__circuit.compute(device_kind)
self.__difference = self.__value - target_value
self.__relative_difference = self.__difference / target_value
self.__cv_trait = cv_trait
@property
def circuit(self) -> Circuit:
@@ -161,16 +66,16 @@ class ResponseItem:
"""
return self.__circuit.device_scale.to_device_count()
@property
@cached_property
def value(self) -> float:
"""
The value of this circuit.
:return: The value.
"""
return self.__value
return self.__cv_trait.value(self.__circuit)
@property
@cached_property
def difference(self) -> float:
"""
The signed difference between the target value and the value of this circuit.
@@ -180,18 +85,18 @@ class ResponseItem:
:return: The signed difference.
"""
return self.__difference
return self.__cv_trait.difference(self.__circuit, value=self.value)
@property
@cached_property
def unsigned_difference(self) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:return: The unsigned difference.
"""
return abs(self.__difference)
return self.__cv_trait.unsigned_difference(self.__circuit, difference=self.difference)
@property
@cached_property
def relative_difference(self) -> float:
"""
The signed relative difference between the target value and the value of this circuit.
@@ -201,16 +106,18 @@ class ResponseItem:
:return: The signed relative difference.
"""
return self.__relative_difference
return self.__cv_trait.relative_difference(self.__circuit, difference=self.difference)
@property
@cached_property
def unsigned_relative_difference(self) -> float:
"""
The unsigned relative difference between the target value and the value of this circuit.
:return: The unsigned relative difference.
"""
return abs(self.__relative_difference)
return self.__cv_trait.unsigned_relative_difference(
self.__circuit, relative_difference=self.relative_difference
)
class Response:
@@ -226,9 +133,9 @@ class Response:
"""The sorted items by priority and difference."""
def __init__(self, request: Request, candidates: Iterable[Circuit]) -> None:
cv_trait = CircuitValueTrait(request.device_kind, request.target_value)
self.__sorted_items = list(
ResponseItem(item, request.device_kind, request.target_value)
for item in candidates
ResponseItem(item, cv_trait) for item in candidates
)
# Sort by different strategy

View File

@@ -1,9 +1,9 @@
from .common import Resolver
from .lut import LutResolver
from .astar import AStarResolver
from .bfs import BfsResolver
__all__ = [
'Resolver',
'LutResolver',
'AStarResolver'
'BfsResolver'
]

View File

@@ -1,17 +0,0 @@
from typing import Iterator
from .common import Resolver
from ..dataset import DatasetCollection
from ..common import Circuit
from ..query import Request, Response
class AStarResolver(Resolver):
"""
A resolver that uses A* algorithm to find the best matching circuit.
"""
def __init__(self, dataset: DatasetCollection):
pass
def resolve(self, request: Request) -> Iterator[Circuit]:
pass

View File

@@ -0,0 +1,259 @@
import heapq
from itertools import chain, combinations_with_replacement, product
from typing import Iterable, Iterator
from functools import cached_property
from .common import Resolver
from ..dataset import DatasetCollection, Dataset
from ..common import Circuit, DeviceKind, JointKind, CircuitValueTrait
from ..query import Request, Response
class BfsItem:
"""
The entry used in BFS iteration storing circuit and value.
"""
__circuit: Circuit
"""The circuit represented by this item."""
__cv_trait: CircuitValueTrait
"""The trait for computing circuit values."""
def __init__(self, circuit: Circuit, cv_trait: CircuitValueTrait):
self.__circuit = circuit
self.__cv_trait = cv_trait
@property
def circuit(self) -> Circuit:
return self.__circuit
@cached_property
def value(self) -> float:
"""
The computed value of the circuit.
:return: The computed value.
"""
return self.__cv_trait.value(self.__circuit)
@cached_property
def unsigned_difference(self) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:return: The unsigned difference.
"""
return self.__cv_trait.unsigned_difference(self.__circuit, value=self.value)
class ResultBucket(Iterable[BfsItem]):
"""
A bounded bucket that keeps up to `N` LutItem entries with the smallest floats.
When the bucket is full, inserting a new item only succeeds if its float
is less than the current maximum; the maximum is then evicted.
"""
class ResultBucketItem:
"""
An item stored in a :class:`ResultBucket`.
"""
__score: float
"""The score associated with this item."""
__item: BfsItem
"""The underlying LutItem."""
__seq: int
"""
Monotonic counter used as a tiebreaker when scores are equal,
ensuring that heapq never compares :class:`LutItem` directly.
"""
def __init__(self, score: float, item: BfsItem, seq: int):
self.__score = score
self.__item = item
self.__seq = seq
@property
def score(self) -> float:
"""The score associated with this item."""
return self.__score
@property
def item(self) -> BfsItem:
"""The underlying LutItem."""
return self.__item
def __lt__(self, other: "ResultBucket.ResultBucketItem") -> bool:
# heapq is a min-heap: it always pops the smallest element.
# We invert the comparison so that an item with a larger score
# is considered "smaller", effectively turning the min-heap
# into a max-heap (largest-score item at the top).
if self.__score != other.__score:
return self.__score > other.__score
# Counter tiebreaker: when scores are equal the later-inserted
# item (higher seq) is considered "smaller" and gets evicted first.
return self.__seq > other.__seq
__n: int
"""Maximum number of items the bucket can hold."""
__heap: list[ResultBucketItem]
"""
Min-heap of :class:`ResultBucketItem`. The heap invariant is inverted
via :meth:`ResultBucketItem.__lt__` so the entry with the largest score
sits at index 0.
"""
__counter: int
"""
Monotonic counter fed to each :class:`ResultBucketItem` as a tiebreaker,
preventing heapq from comparing :class:`LutItem` on score collisions.
"""
def __init__(self, n: int):
self.__n = n
self.__heap = []
self.__counter = 0
def __len__(self) -> int:
return len(self.__heap)
def __iter__(self) -> Iterator[BfsItem]:
for entry in self.__heap:
yield entry.item
def insert(self, item: BfsItem, score: float) -> bool:
"""
Insert a :class:`LutItem` with the given score.
If the bucket is not yet full the item is always inserted.
Otherwise the item is only inserted when *score* is smaller
than the largest score currently in the bucket; the entry
with the largest score is then evicted.
:param item: The LutItem to insert.
:param score: The score associated with the item.
:return: ``True`` if the item was inserted, ``False`` otherwise.
"""
entry = ResultBucket.ResultBucketItem(score, item, self.__counter)
if len(self.__heap) < self.__n:
heapq.heappush(self.__heap, entry)
self.__counter += 1
return True
if score >= self.__heap[0].score:
return False
heapq.heapreplace(self.__heap, entry)
self.__counter += 1
return True
class BfsResolver(Resolver):
__datasets: DatasetCollection
def __init__(self, datasets: DatasetCollection):
self.__datasets = datasets
# YYC MARK:
# Some circuit are equivalent in topology.
# If we deduplicate these equaivalent circuit in building result,
# there are too complex works.
# So we should deduplicated these equivalent circuit at the beginning,
# i.e. when generating them.
# So following 3 function are taking this job.
@staticmethod
def iter_one_device_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with one device without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with one device.
"""
# Every single device is unique so we directly output them.
# This feature is insured by dataset itself.
return (Circuit.from_one_device(v1) for v1 in dataset.values)
@staticmethod
def iter_two_devices_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with two devices without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with two devices.
"""
# The two devices in this circuit is always swapable,
# so we iterate them without repeating.
return (
Circuit.from_two_devices(v1, v2, j2)
for (v1, v2), j2 in product(
combinations_with_replacement(dataset.values, 2),
tuple(JointKind),
)
)
@staticmethod
def iter_three_devices_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with three devices without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with three devices.
"""
# For generating three devices circuit,
# it should be consisted by 2 parts.
return chain(
# First, the whole circuit has only one joint type.
# In this case, 3 devices are swapable and we should iterate them without repeating
(
Circuit.from_three_devices(v1, v2, j, v3, j)
for (v1, v2, v3), j in product(
combinations_with_replacement(dataset.values, 3),
tuple(JointKind),
)
),
# Second, if the joint type is different, then the first 2 devices are swapable.
# So we need iterate them without repeating.
(
Circuit.from_three_devices(v1, v2, j, v3, j.flip())
for (v1, v2), v3, j in product(
combinations_with_replacement(dataset.values, 2),
dataset.values,
tuple(JointKind),
)
),
)
@staticmethod
def __bfs_iteration(
dataset: Dataset, cv_trait: CircuitValueTrait
) -> Iterator[BfsItem]:
return (
BfsItem(circuit, cv_trait)
for circuit in chain(
BfsResolver.iter_one_device_circuit(dataset),
BfsResolver.iter_two_devices_circuit(dataset),
BfsResolver.iter_three_devices_circuit(dataset),
)
)
def resolve(self, request: Request) -> Response:
# Pick dataset from collection
dataset: Dataset
match request.device_kind:
case DeviceKind.RESISTOR:
dataset = self.__datasets.resistor_values
case DeviceKind.CAPACITOR:
dataset = self.__datasets.capacitor_values
case DeviceKind.INDUCTOR:
dataset = self.__datasets.inductor_values
# Iterate circuit item one by one
bucket = ResultBucket(request.count_limit)
cv_trait = CircuitValueTrait(request.device_kind, request.target_value)
for item in BfsResolver.__bfs_iteration(dataset, cv_trait):
# If circuit absolute difference is out of tolerance, skip it directly.
if item.unsigned_difference > request.tolerance:
continue
# put it into bucket
bucket.insert(item, item.unsigned_difference)
# Return result
return Response(request, map(lambda item: item.circuit, bucket))

View File

@@ -2,9 +2,10 @@ 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
from ..query import Request, Response, ResponseDeduper
from ..common import Circuit, DeviceKind, JointKind, CircuitValueTrait
from ..query import Request, Response
class LutItem:
@@ -14,25 +15,20 @@ class LutItem:
__circuit: Circuit
"""The circuit represented by this item."""
__device_kind: DeviceKind
"""The device kind applied for this circuit."""
__value: float
"""The value of this circuit."""
def __init__(self, circuit: Circuit, device_kind: DeviceKind):
self.__circuit = circuit
self.__device_kind = device_kind
self.__value = self.__circuit.compute(device_kind)
@property
def circuit(self) -> Circuit:
return self.__circuit
@cached_property
@property
def value(self) -> float:
"""
The computed value of the circuit.
:return: The computed value.
"""
return self.__circuit.compute(self.__device_kind)
return self.__value
class LutResolver(Resolver):
@@ -59,22 +55,12 @@ class LutResolver(Resolver):
)
def __build_lut(self, dataset: Dataset, device_kind: DeviceKind) -> list[LutItem]:
values = dataset.values
joints = tuple(JointKind)
lut = [
LutItem(circuit, device_kind)
for circuit in chain(
(Circuit.from_one_device(v1) for v1 in values),
(
Circuit.from_two_devices(v1, v2, j2)
for v1, v2, j2 in product(values, values, joints)
),
(
Circuit.from_three_devices(v1, v2, j2, v3, j3)
for v1, v2, j2, v3, j3 in product(
values, values, joints, values, joints
)
),
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)
@@ -91,8 +77,8 @@ class LutResolver(Resolver):
lut = self.__inductor_lut
target = request.target_value
count_limit = min(request.count_limit, 100)
deduper = ResponseDeduper()
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.
@@ -106,7 +92,7 @@ class LutResolver(Resolver):
# difference order, so the first N items within tolerance are exactly
# the N best matches.
while left >= 0 or right < len(lut):
if len(deduper) >= count_limit:
if len(bucket) >= count_limit:
break
if left < 0:
@@ -134,6 +120,6 @@ class LutResolver(Resolver):
right = len(lut)
continue
deduper.add(item.circuit)
bucket.append(item.circuit)
return Response(request, deduper)
return Response(request, bucket)