vecwire 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vecwire/__init__.py +14 -0
- vecwire/core.py +529 -0
- vecwire/elements/__init__.py +24 -0
- vecwire/elements/ground.py +43 -0
- vecwire/elements/junctions.py +270 -0
- vecwire/elements/passive.py +62 -0
- vecwire/geometry.py +120 -0
- vecwire/node.py +22 -0
- vecwire/py.typed +0 -0
- vecwire-0.3.0.dist-info/METADATA +281 -0
- vecwire-0.3.0.dist-info/RECORD +13 -0
- vecwire-0.3.0.dist-info/WHEEL +4 -0
- vecwire-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Josephson junctions: the bare X, boxed harmonic junctions, and their sums.
|
|
2
|
+
|
|
3
|
+
``junction`` draws the standard symbol, a bare X, which says nothing about the
|
|
4
|
+
current-phase relation. ``harmonic_junction`` draws the boxed symbol of one
|
|
5
|
+
tunnelling harmonic: a square crossed by ``m`` diagonals each way stands for a
|
|
6
|
+
``cos(m phi)`` term, in which ``m`` Cooper pairs tunnel together, and two dots
|
|
7
|
+
mark the odd ``sin(m phi)`` term of a phi0 junction. For ``m = 1`` the boxed
|
|
8
|
+
symbol is the bare X inside its square. ``harmonic_sum`` composes boxed glyphs
|
|
9
|
+
into a multiharmonic junction.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
from itertools import pairwise
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from ..geometry import Box, Branch, Point, positive, positive_int
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from lxml import etree
|
|
23
|
+
|
|
24
|
+
from ..core import Circuit, NodeRef
|
|
25
|
+
|
|
26
|
+
JUNCTION_KINDS = ("cos", "sin")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _kind(value: str) -> str:
|
|
30
|
+
if value not in JUNCTION_KINDS:
|
|
31
|
+
raise ValueError("kind must be 'cos' or 'sin'")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _glyph_branch(
|
|
36
|
+
circuit: Circuit, start: NodeRef, end: NodeRef, size: float
|
|
37
|
+
) -> tuple[Branch, float]:
|
|
38
|
+
"""Return the branch and the checked glyph size, which must fit between the terminals."""
|
|
39
|
+
branch = circuit.branch(start, end)
|
|
40
|
+
size = positive(size, "size")
|
|
41
|
+
if size >= branch.length:
|
|
42
|
+
raise ValueError("Junction glyph must be shorter than terminal separation")
|
|
43
|
+
return branch, size
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _leads(
|
|
47
|
+
circuit: Circuit, group: etree._Element, branch: Branch, size: float, color: str | None
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Draw the two leads from the terminals to the edge of a centred box of side ``size``."""
|
|
50
|
+
centre = branch.length / 2
|
|
51
|
+
circuit.line(group, branch.start.position, branch.at(centre - size / 2), color=color)
|
|
52
|
+
circuit.line(group, branch.at(centre + size / 2), branch.end.position, color=color)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def junction(
|
|
56
|
+
circuit: Circuit,
|
|
57
|
+
start: NodeRef,
|
|
58
|
+
end: NodeRef,
|
|
59
|
+
*,
|
|
60
|
+
id: str | None = None,
|
|
61
|
+
size: float = 7.2,
|
|
62
|
+
color: str | None = None,
|
|
63
|
+
lead_color: str | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Draw a Josephson junction as the bare X, with two leads.
|
|
66
|
+
|
|
67
|
+
The X spans the diagonals of a ``size`` square centred on the branch. The
|
|
68
|
+
leads run on to the centre of the X, so the wire is continuous through it.
|
|
69
|
+
``color`` sets the X and, unless ``lead_color`` is given, the leads.
|
|
70
|
+
"""
|
|
71
|
+
branch, size = _glyph_branch(circuit, start, end, size)
|
|
72
|
+
group = circuit.group("josephson-junction", id, branch=branch)
|
|
73
|
+
centre = branch.length / 2
|
|
74
|
+
middle = branch.at(centre)
|
|
75
|
+
leads = lead_color or color
|
|
76
|
+
circuit.line(group, branch.start.position, middle, color=leads)
|
|
77
|
+
circuit.line(group, middle, branch.end.position, color=leads)
|
|
78
|
+
for sign in (-1, 1):
|
|
79
|
+
circuit.line(
|
|
80
|
+
group,
|
|
81
|
+
branch.at(centre - size / 2, sign * size / 2),
|
|
82
|
+
branch.at(centre + size / 2, -sign * size / 2),
|
|
83
|
+
color=color,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def harmonic_junction(
|
|
88
|
+
circuit: Circuit,
|
|
89
|
+
start: NodeRef,
|
|
90
|
+
end: NodeRef,
|
|
91
|
+
order: int = 1,
|
|
92
|
+
*,
|
|
93
|
+
kind: str = "cos",
|
|
94
|
+
id: str | None = None,
|
|
95
|
+
size: float = 7.2,
|
|
96
|
+
color: str | None = None,
|
|
97
|
+
lead_color: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Draw the boxed junction symbol of one tunnelling harmonic, with two leads.
|
|
100
|
+
|
|
101
|
+
The square is crossed by ``order`` diagonals in each direction, for a
|
|
102
|
+
``cos(order * phi)`` term that transfers ``order`` Cooper pairs.
|
|
103
|
+
``kind="sin"`` adds two dots for the odd ``sin(order * phi)`` term of a
|
|
104
|
+
phi0 junction. ``color`` sets the glyph and, unless ``lead_color`` is
|
|
105
|
+
given, the leads. The group records ``data-harmonic`` and ``data-kind``.
|
|
106
|
+
"""
|
|
107
|
+
branch, size = _glyph_branch(circuit, start, end, size)
|
|
108
|
+
kind = _kind(kind)
|
|
109
|
+
order = positive_int(order, "order")
|
|
110
|
+
group = circuit.group(
|
|
111
|
+
"josephson-junction", id, branch=branch, data={"harmonic": order, "kind": kind}
|
|
112
|
+
)
|
|
113
|
+
_leads(circuit, group, branch, size, lead_color or color)
|
|
114
|
+
harmonic_glyph(circuit, group, branch.at(branch.length / 2), size, order, kind, color)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def harmonic_glyph(
|
|
118
|
+
circuit: Circuit,
|
|
119
|
+
parent: etree._Element,
|
|
120
|
+
center: Point,
|
|
121
|
+
size: float,
|
|
122
|
+
order: int,
|
|
123
|
+
kind: str = "cos",
|
|
124
|
+
color: str | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Draw the boxed harmonic glyph, without leads, into ``parent``.
|
|
127
|
+
|
|
128
|
+
A square of side ``size`` holds ``order`` parallel diagonals each way,
|
|
129
|
+
clipped to the square; ``kind="sin"`` adds the two dots. The glyph is
|
|
130
|
+
symmetric under quarter turns, so it needs no orientation.
|
|
131
|
+
"""
|
|
132
|
+
half = size / 2
|
|
133
|
+
cx, cy = center
|
|
134
|
+
circuit.rect(parent, (cx - half, cy - half), (cx + half, cy + half), color=color)
|
|
135
|
+
# Diagonals b = s*a + c in local coordinates, clipped to the square.
|
|
136
|
+
pitch = 0.9 * size / order
|
|
137
|
+
hatch = circuit.stroke_width * (1 if order == 1 else 0.75)
|
|
138
|
+
for slope in (1, -1):
|
|
139
|
+
for index in range(order):
|
|
140
|
+
c = (index - (order - 1) / 2) * pitch
|
|
141
|
+
lo, hi = max(-half, -half - slope * c), min(half, half - slope * c)
|
|
142
|
+
circuit.line(
|
|
143
|
+
parent,
|
|
144
|
+
(cx + lo, cy + slope * lo + c),
|
|
145
|
+
(cx + hi, cy + slope * hi + c),
|
|
146
|
+
color=color,
|
|
147
|
+
width=hatch,
|
|
148
|
+
)
|
|
149
|
+
if kind == "sin":
|
|
150
|
+
for sign in (-1, 1):
|
|
151
|
+
circuit.dot(parent, (cx + sign * 0.27 * size, cy), 0.075 * size + 0.1, color)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def harmonic_sum(
|
|
155
|
+
circuit: Circuit,
|
|
156
|
+
start: NodeRef,
|
|
157
|
+
end: NodeRef,
|
|
158
|
+
orders: Sequence[int] = (1, 2, 3),
|
|
159
|
+
*,
|
|
160
|
+
id: str | None = None,
|
|
161
|
+
kinds: str | Sequence[str] = "cos",
|
|
162
|
+
colors: str | Sequence[str | None] | None = None,
|
|
163
|
+
ellipsis: bool = True,
|
|
164
|
+
size: float = 7.2,
|
|
165
|
+
spacing: float | None = None,
|
|
166
|
+
padding: float = 2.4,
|
|
167
|
+
lead_color: str | None = None,
|
|
168
|
+
box_color: str | None = None,
|
|
169
|
+
dash: Sequence[float] = (2.2, 1.6),
|
|
170
|
+
corner_radius: float = 2,
|
|
171
|
+
rails: bool = True,
|
|
172
|
+
) -> Box:
|
|
173
|
+
"""Draw a multiharmonic junction as a dashed box holding a sum of harmonics.
|
|
174
|
+
|
|
175
|
+
The box is centred on the branch between the terminals; the sum
|
|
176
|
+
``m=1 + m=2 + ...`` reads left to right on a vertical branch and top to
|
|
177
|
+
bottom on a horizontal one. Each term is a boxed ``harmonic_glyph``. The
|
|
178
|
+
harmonics of one junction add as energies, so the terms are elements in
|
|
179
|
+
parallel: by default each term hangs between two rails inside the box, and
|
|
180
|
+
the branch leads join the rails. ``rails=False`` instead ends each term's
|
|
181
|
+
leads in terminal dots, a shorthand sum notation. Returns the box bounds;
|
|
182
|
+
``Box.terms`` holds the glyph centres for per-term labels.
|
|
183
|
+
"""
|
|
184
|
+
branch = circuit.branch(start, end)
|
|
185
|
+
if not orders:
|
|
186
|
+
raise ValueError("orders must contain at least one harmonic")
|
|
187
|
+
orders = [positive_int(order, "order") for order in orders]
|
|
188
|
+
count = len(orders)
|
|
189
|
+
kinds = [kinds] * count if isinstance(kinds, str) else list(kinds)
|
|
190
|
+
palette = [colors] * count if colors is None or isinstance(colors, str) else list(colors)
|
|
191
|
+
if len(kinds) != count or len(palette) != count:
|
|
192
|
+
raise ValueError("kinds and colors must match the number of orders")
|
|
193
|
+
kinds = [_kind(kind) for kind in kinds]
|
|
194
|
+
size = positive(size, "size")
|
|
195
|
+
spacing = 2.2 * size if spacing is None else positive(spacing, "spacing")
|
|
196
|
+
padding = positive(padding, "padding")
|
|
197
|
+
stub = 0.45 * size
|
|
198
|
+
half_along = size / 2 + stub + padding
|
|
199
|
+
if 2 * half_along >= branch.length:
|
|
200
|
+
raise ValueError("harmonic_sum box must be shorter than terminal separation")
|
|
201
|
+
# Across offsets: glyph centres, then an optional "+ ..." tail.
|
|
202
|
+
normal = branch.normal
|
|
203
|
+
reading = (abs(normal[0]), abs(normal[1]))
|
|
204
|
+
centres = [index * spacing for index in range(count)]
|
|
205
|
+
extent = [-size / 2, centres[-1] + size / 2]
|
|
206
|
+
if ellipsis:
|
|
207
|
+
dots_at = centres[-1] + spacing * 0.95
|
|
208
|
+
extent[1] = dots_at + 0.3 * size + 0.6
|
|
209
|
+
shift = -(extent[0] + extent[1]) / 2
|
|
210
|
+
mid = branch.length / 2
|
|
211
|
+
|
|
212
|
+
def place(along: float, across: float) -> Point:
|
|
213
|
+
"""Point at ``along`` from the box centre and ``across`` in reading order."""
|
|
214
|
+
x, y = branch.at(mid + along)
|
|
215
|
+
return x + reading[0] * (across + shift), y + reading[1] * (across + shift)
|
|
216
|
+
|
|
217
|
+
group = circuit.group(
|
|
218
|
+
"harmonic-sum", id, branch=branch, data={"orders": " ".join(map(str, orders))}
|
|
219
|
+
)
|
|
220
|
+
reach = size / 2 + stub # glyph centre to a rail or terminal dot
|
|
221
|
+
outer = reach if rails else half_along
|
|
222
|
+
circuit.line(group, branch.start.position, place(-outer, -shift), color=lead_color)
|
|
223
|
+
circuit.line(group, place(outer, -shift), branch.end.position, color=lead_color)
|
|
224
|
+
bounds = circuit.rect(
|
|
225
|
+
group,
|
|
226
|
+
place(-half_along, extent[0] - padding),
|
|
227
|
+
place(half_along, extent[1] + padding),
|
|
228
|
+
color=box_color,
|
|
229
|
+
width=0.75 * circuit.stroke_width,
|
|
230
|
+
corner_radius=corner_radius,
|
|
231
|
+
dash=dash,
|
|
232
|
+
)
|
|
233
|
+
if rails:
|
|
234
|
+
# The rails run on to the ellipsis: further terms hang there too.
|
|
235
|
+
span = (min(centres[0], -shift), max(dots_at if ellipsis else centres[-1], -shift))
|
|
236
|
+
for sign, side in ((-1, "top"), (1, "bottom")):
|
|
237
|
+
rail = circuit.group("rail", None if id is None else f"{id}-rail-{side}", parent=group)
|
|
238
|
+
circuit.line(
|
|
239
|
+
rail, place(sign * reach, span[0]), place(sign * reach, span[1]), color=lead_color
|
|
240
|
+
)
|
|
241
|
+
dot_radius = 0.9 * circuit.stroke_width
|
|
242
|
+
terms = []
|
|
243
|
+
for order, kind, color, across in zip(orders, kinds, palette, centres, strict=True):
|
|
244
|
+
term = circuit.group(
|
|
245
|
+
"josephson-junction",
|
|
246
|
+
None if id is None else f"{id}-m{order}",
|
|
247
|
+
parent=group,
|
|
248
|
+
data={"harmonic": order, "kind": kind},
|
|
249
|
+
)
|
|
250
|
+
for sign in (-1, 1):
|
|
251
|
+
tip = place(sign * reach, across)
|
|
252
|
+
circuit.line(term, place(sign * size / 2, across), tip, color=lead_color)
|
|
253
|
+
if not rails:
|
|
254
|
+
circuit.dot(term, tip, dot_radius, lead_color)
|
|
255
|
+
harmonic_glyph(circuit, term, place(0, across), size, order, kind, color)
|
|
256
|
+
terms.append(place(0, across))
|
|
257
|
+
signs = [(left + right) / 2 for left, right in pairwise(centres)]
|
|
258
|
+
if ellipsis:
|
|
259
|
+
signs.append(centres[-1] + spacing * 0.45)
|
|
260
|
+
arm = 0.28 * size
|
|
261
|
+
for across in signs:
|
|
262
|
+
cx, cy = place(0, across)
|
|
263
|
+
plus = circuit.group("plus", parent=group)
|
|
264
|
+
circuit.line(plus, (cx - arm, cy), (cx + arm, cy), color=lead_color)
|
|
265
|
+
circuit.line(plus, (cx, cy - arm), (cx, cy + arm), color=lead_color)
|
|
266
|
+
if ellipsis:
|
|
267
|
+
more = circuit.group("ellipsis", parent=group)
|
|
268
|
+
for step in (-1, 0, 1):
|
|
269
|
+
circuit.dot(more, place(0, dots_at + step * 0.3 * size), 0.6, lead_color)
|
|
270
|
+
return replace(bounds, terms=tuple(terms))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Capacitor and inductor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..geometry import positive, positive_int
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..core import Circuit, NodeRef, PathCommand
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def capacitor(
|
|
14
|
+
circuit: Circuit,
|
|
15
|
+
start: NodeRef,
|
|
16
|
+
end: NodeRef,
|
|
17
|
+
*,
|
|
18
|
+
id: str | None = None,
|
|
19
|
+
plate_width: float = 14,
|
|
20
|
+
gap: float = 4,
|
|
21
|
+
color: str | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Draw a two-terminal capacitor with leads and parallel plates."""
|
|
24
|
+
branch = circuit.branch(start, end)
|
|
25
|
+
plate_width, gap = positive(plate_width, "plate_width"), positive(gap, "gap")
|
|
26
|
+
if gap >= branch.length:
|
|
27
|
+
raise ValueError("Capacitor gap must be shorter than terminal separation")
|
|
28
|
+
group = circuit.group("capacitor", id, branch=branch)
|
|
29
|
+
first, second = (branch.length - gap) / 2, (branch.length + gap) / 2
|
|
30
|
+
circuit.line(group, branch.start.position, branch.at(first), color=color)
|
|
31
|
+
circuit.line(group, branch.at(second), branch.end.position, color=color)
|
|
32
|
+
for along in (first, second):
|
|
33
|
+
circuit.line(
|
|
34
|
+
group,
|
|
35
|
+
branch.at(along, -plate_width / 2),
|
|
36
|
+
branch.at(along, plate_width / 2),
|
|
37
|
+
color=color,
|
|
38
|
+
width=1.3,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def inductor(
|
|
43
|
+
circuit: Circuit,
|
|
44
|
+
start: NodeRef,
|
|
45
|
+
end: NodeRef,
|
|
46
|
+
*,
|
|
47
|
+
id: str | None = None,
|
|
48
|
+
turns: int = 4,
|
|
49
|
+
color: str | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Draw an inductor as consecutive half-turns between its terminals."""
|
|
52
|
+
branch = circuit.branch(start, end)
|
|
53
|
+
turns = positive_int(turns, "turns")
|
|
54
|
+
group = circuit.group("inductor", id, branch=branch)
|
|
55
|
+
radius = branch.length / (2 * turns)
|
|
56
|
+
# Bulge to the same side of the screen whatever the terminal order.
|
|
57
|
+
dx, dy = branch.direction
|
|
58
|
+
sweep = 1 if dy > 0 or dx < 0 else 0
|
|
59
|
+
commands: list[PathCommand] = [("M", *branch.start.position)]
|
|
60
|
+
for index in range(1, turns + 1):
|
|
61
|
+
commands.append(("A", radius, radius, 0, 0, sweep, *branch.at(2 * radius * index)))
|
|
62
|
+
circuit.path(group, commands, color=color)
|
vecwire/geometry.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Points, bounds, and branches shared by the canvas and the symbol library.
|
|
2
|
+
|
|
3
|
+
This module imports nothing else from vecwire except ``Node``, so both the core
|
|
4
|
+
canvas and the symbols in ``vecwire.elements`` can use it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from math import hypot, isfinite
|
|
11
|
+
|
|
12
|
+
from .node import Node
|
|
13
|
+
|
|
14
|
+
Point = tuple[float, float]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def point(value: Point) -> Point:
|
|
18
|
+
"""Return ``value`` as a pair of finite floats, or raise ``ValueError``."""
|
|
19
|
+
if len(value) != 2 or not all(isfinite(v) for v in value):
|
|
20
|
+
raise ValueError("A point must contain two finite coordinates")
|
|
21
|
+
return float(value[0]), float(value[1])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def positive(value: float, name: str) -> float:
|
|
25
|
+
"""Return ``value`` as a float if it is finite and positive."""
|
|
26
|
+
if not isfinite(value) or value <= 0:
|
|
27
|
+
raise ValueError(f"{name} must be finite and positive")
|
|
28
|
+
return float(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def nonnegative(value: float, name: str) -> float:
|
|
32
|
+
"""Return ``value`` as a float if it is finite and not negative."""
|
|
33
|
+
if not isfinite(value) or value < 0:
|
|
34
|
+
raise ValueError(f"{name} must be finite and nonnegative")
|
|
35
|
+
return float(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def positive_int(value: int, name: str) -> int:
|
|
39
|
+
"""Return ``value`` if it is a positive integer (``bool`` is rejected)."""
|
|
40
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
41
|
+
raise ValueError(f"{name} must be a positive integer")
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def offset(
|
|
46
|
+
origin: Point, direction: Point, normal: Point, along: float, across: float = 0.0
|
|
47
|
+
) -> Point:
|
|
48
|
+
"""Move ``along`` units in ``direction`` and ``across`` units along ``normal``."""
|
|
49
|
+
return (
|
|
50
|
+
origin[0] + direction[0] * along + normal[0] * across,
|
|
51
|
+
origin[1] + direction[1] * along + normal[1] * across,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class Box:
|
|
57
|
+
"""Axis-aligned bounds of a drawn element, for placing labels next to it.
|
|
58
|
+
|
|
59
|
+
``terms`` lists the glyph centres of a ``harmonic_sum`` in reading order.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
left: float
|
|
63
|
+
top: float
|
|
64
|
+
right: float
|
|
65
|
+
bottom: float
|
|
66
|
+
terms: tuple[Point, ...] = ()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def center(self) -> Point:
|
|
70
|
+
return (self.left + self.right) / 2, (self.top + self.bottom) / 2
|
|
71
|
+
|
|
72
|
+
def at(self, anchor: str = "cc") -> Point:
|
|
73
|
+
"""Return a point on the box using the ``l/c/r`` + ``t/c/b`` label anchors."""
|
|
74
|
+
if len(anchor) != 2 or anchor[0] not in "lcr" or anchor[1] not in "tcb":
|
|
75
|
+
raise ValueError("anchor must combine l/c/r with t/c/b")
|
|
76
|
+
x = {"l": self.left, "c": (self.left + self.right) / 2, "r": self.right}[anchor[0]]
|
|
77
|
+
y = {"t": self.top, "c": (self.top + self.bottom) / 2, "b": self.bottom}[anchor[1]]
|
|
78
|
+
return x, y
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class Branch:
|
|
83
|
+
"""The straight, horizontal or vertical run between two terminal nodes.
|
|
84
|
+
|
|
85
|
+
Two-terminal symbols are drawn in branch coordinates: ``along`` is the
|
|
86
|
+
distance from ``start`` towards ``end`` and ``across`` the distance along
|
|
87
|
+
``normal``, the direction a quarter turn clockwise on screen. ``at(along,
|
|
88
|
+
across)`` converts to drawing coordinates, so one symbol definition serves
|
|
89
|
+
every orientation.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
start: Node
|
|
93
|
+
end: Node
|
|
94
|
+
|
|
95
|
+
def __post_init__(self) -> None:
|
|
96
|
+
dx, dy = self.end.x - self.start.x, self.end.y - self.start.y
|
|
97
|
+
if dx == 0 and dy == 0:
|
|
98
|
+
raise ValueError("Component terminals must be distinct")
|
|
99
|
+
if dx != 0 and dy != 0:
|
|
100
|
+
raise ValueError("Components must be horizontal or vertical")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def length(self) -> float:
|
|
104
|
+
return hypot(self.end.x - self.start.x, self.end.y - self.start.y)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def direction(self) -> Point:
|
|
108
|
+
"""Unit vector from ``start`` to ``end``."""
|
|
109
|
+
length = self.length
|
|
110
|
+
return (self.end.x - self.start.x) / length, (self.end.y - self.start.y) / length
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def normal(self) -> Point:
|
|
114
|
+
"""Unit vector perpendicular to ``direction``."""
|
|
115
|
+
dx, dy = self.direction
|
|
116
|
+
return -dy, dx
|
|
117
|
+
|
|
118
|
+
def at(self, along: float, across: float = 0.0) -> Point:
|
|
119
|
+
"""Return the drawing coordinates of a point given in branch coordinates."""
|
|
120
|
+
return offset(self.start.position, self.direction, self.normal, along, across)
|
vecwire/node.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Placed connection points in a circuit schematic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Node:
|
|
10
|
+
"""A named connection point at one position in the drawing.
|
|
11
|
+
|
|
12
|
+
Wires may join several placed nodes into one electrical net. A node is a
|
|
13
|
+
connection point, not a claim that its name identifies an entire net.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
x: float
|
|
18
|
+
y: float
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def position(self) -> tuple[float, float]:
|
|
22
|
+
return self.x, self.y
|
vecwire/py.typed
ADDED
|
File without changes
|