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.
@@ -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