knit-graphs 0.0.10__py3-none-any.whl → 0.0.12__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,454 @@
1
+ """Module containing directed loop graph class"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from dataclasses import dataclass, field
7
+ from typing import Generic, TypeVar, cast, overload
8
+
9
+ from networkx import DiGraph, ancestors, dfs_edges, dfs_preorder_nodes, has_path
10
+
11
+ from knit_graphs.Loop import Loop
12
+ from knit_graphs.Pull_Direction import Pull_Direction
13
+
14
+ EdgeT = TypeVar("EdgeT")
15
+
16
+ LoopT = TypeVar("LoopT", bound=Loop)
17
+
18
+
19
+ class Directed_Loop_Graph(Generic[LoopT, EdgeT]):
20
+ """
21
+ Wrapper for networkx.DiGraphs with directed edges between loops (i.e., floats in yarns, stitches in knitgraph).
22
+ """
23
+
24
+ _DATA_ATTRIBUTE_NAME = "data"
25
+
26
+ def __init__(self) -> None:
27
+ self._loop_graph: DiGraph = DiGraph()
28
+ self._loops_by_loop_id: dict[int, LoopT] = {}
29
+
30
+ @property
31
+ def loop_count(self) -> int:
32
+ """
33
+ Returns:
34
+ int: The number of loops in the graph.
35
+ """
36
+ return len(self._loops_by_loop_id)
37
+
38
+ @property
39
+ def edge_count(self) -> int:
40
+ """
41
+ Returns:
42
+ int: The number of edges in the graph.
43
+ """
44
+ return len(self._loop_graph.edges)
45
+
46
+ @property
47
+ def contains_loops(self) -> bool:
48
+ """
49
+ Returns:
50
+ bool: True if the graph has at least one loop, False otherwise.
51
+ """
52
+ return len(self) > 0
53
+
54
+ @property
55
+ def sorted_loops(self) -> list[LoopT]:
56
+ """
57
+ Returns:
58
+ list[Loop]: The list of loops in the graph sorted from the earliest formed loop to the latest formed loop.
59
+ """
60
+ return sorted(self)
61
+
62
+ @property
63
+ def edge_iter(self) -> Iterator[tuple[LoopT, LoopT, EdgeT]]:
64
+ """
65
+ Returns:
66
+ Iterator[tuple[LoopT, LoopT, EdgeT]]: Iterator over the edges and edge-data in the graph.
67
+
68
+ Notes:
69
+ No guarantees about the order of the edges.
70
+ """
71
+ return iter((cast(LoopT, u), cast(LoopT, v), self.get_edge(u, v)) for u, v in self._loop_graph.edges)
72
+
73
+ @property
74
+ def terminal_loops(self) -> Iterator[LoopT]:
75
+ """
76
+ Returns:
77
+ Iterator[Loop]: An iterator over all terminal loops in the graph.
78
+ """
79
+ return iter(loop for loop in self if self.is_terminal_loop(loop))
80
+
81
+ def has_loop(self, loop: int | LoopT) -> bool:
82
+ """
83
+ Args:
84
+ loop (int | LoopT): The loop or loop id to check for in the graph.
85
+
86
+ Returns:
87
+ bool: True if the loop id is in the graph. False, otherwise.
88
+ """
89
+ if isinstance(loop, int):
90
+ return loop in self._loops_by_loop_id
91
+ else:
92
+ return bool(self._loop_graph.has_node(loop))
93
+
94
+ def get_loop(self, loop_id: int) -> LoopT:
95
+ """
96
+ Args:
97
+ loop_id (int): The loop id of the loop to get from the graph.
98
+
99
+ Returns:
100
+ LoopT: The loop node in the graph.
101
+ """
102
+ return self._loops_by_loop_id[loop_id]
103
+
104
+ def successors(self, loop: int | LoopT) -> set[LoopT]:
105
+ """
106
+ Args:
107
+ loop (int | LoopT): The loop to get the successors of from the graph.
108
+
109
+ Returns:
110
+ set[LoopT]: The successors of the loop.
111
+ """
112
+ if isinstance(loop, int):
113
+ loop = self.get_loop(loop)
114
+ return cast(set[LoopT], set(self._loop_graph.successors(loop)))
115
+
116
+ def has_child_loop(self, loop: LoopT) -> bool:
117
+ """
118
+ Args:
119
+ loop (Loop): The loop to check for child connections.
120
+
121
+ Returns:
122
+ bool: True if the loop has a child loop, False otherwise.
123
+ """
124
+ return len(self.successors(loop)) > 0
125
+
126
+ def is_terminal_loop(self, loop: LoopT) -> bool:
127
+ """
128
+ Args:
129
+ loop (LoopT): The loop to check for terminal status.
130
+
131
+ Returns:
132
+ bool: True if the loop has no child loops, False otherwise.
133
+ """
134
+ return not self.has_child_loop(loop)
135
+
136
+ def get_child_loop(self, loop: LoopT) -> LoopT | None:
137
+ """
138
+ Args:
139
+ loop (LoopT): The loop to look for a child loop from.
140
+
141
+ Returns:
142
+ LoopT | None: The child loop if one exists, or None if no child loop is found.
143
+ """
144
+ successors = self.successors(loop)
145
+ if len(successors) == 0:
146
+ return None
147
+ return successors.pop()
148
+
149
+ def predecessors(self, loop: int | LoopT) -> set[LoopT]:
150
+ """
151
+ Args:
152
+ loop (int | LoopT): The loop to get the predecessors of from the graph.
153
+
154
+ Returns:
155
+ set[LoopT]: The successors of the loop.
156
+ """
157
+ if isinstance(loop, int):
158
+ loop = self.get_loop(loop)
159
+ return cast(set[LoopT], set(self._loop_graph.predecessors(loop)))
160
+
161
+ def ancestors(self, loop: LoopT) -> set[LoopT]:
162
+ """
163
+ Args:
164
+ loop (LoopT): The loop to get the ancestors of from the graph.
165
+
166
+ Returns:
167
+ set[LoopT]: The ancestors of the given loop.
168
+ """
169
+ return cast(set[LoopT], ancestors(self._loop_graph, loop))
170
+
171
+ def is_descendant(self, ancestor: LoopT, descendant: LoopT) -> bool:
172
+ """
173
+
174
+ Args:
175
+ ancestor (LoopT): The loop to check if it is an ancestor of the other loop.
176
+ descendant (LoopT): The loop to check if it is a descendant of the other loop.
177
+
178
+ Returns:
179
+ bool: True if there is a directed path from the ancestor to the descendant, False otherwise.
180
+ """
181
+ return bool(has_path(self._loop_graph, ancestor, descendant))
182
+
183
+ def source_loops(self, loop: LoopT) -> set[LoopT]:
184
+ """
185
+ Args:
186
+ loop (LoopT): The loop to get the sources of.
187
+
188
+ Returns:
189
+ set[LoopT]: The source loops of the given loop. These are the loops that are the source of all ancestors to the given loop.
190
+ """
191
+ ancestor_loops = self.ancestors(loop)
192
+ if len(ancestor_loops) == 0:
193
+ return set()
194
+ sources = set()
195
+ while len(ancestor_loops) > 0:
196
+ ancestor = ancestor_loops.pop()
197
+ if len(ancestor_loops) == 0:
198
+ sources.add(ancestor)
199
+ elif not any(self.is_descendant(other, ancestor) for other in ancestor_loops):
200
+ sources.add(ancestor)
201
+ non_sources = {descendant for descendant in ancestor_loops if self.is_descendant(ancestor, descendant)}
202
+ ancestor_loops.difference_update(non_sources) # remove all ancestors that descend from the source.
203
+ return sources
204
+
205
+ def dfs_edges(self, loop: LoopT) -> Iterator[tuple[LoopT, LoopT]]:
206
+ """
207
+ Args:
208
+ loop (LoopT): The loop to start iteration over edges from.
209
+
210
+ Returns:
211
+ The depth-first-search ordering of edges starting from the given loop.
212
+ """
213
+ return cast(Iterator[tuple[LoopT, LoopT]], dfs_edges(self._loop_graph, loop))
214
+
215
+ def dfs_preorder_loops(self, loop: LoopT) -> Iterator[LoopT]:
216
+ """
217
+ Args:
218
+ loop (LoopT): The loop to start iteration from.
219
+
220
+ Returns:
221
+ Iterator[LoopT]: The depth-first-search ordering of loops in the graph starting from the given loop.
222
+ """
223
+ return cast(Iterator[LoopT], dfs_preorder_nodes(self._loop_graph, loop))
224
+
225
+ def has_edge(self, u: LoopT | int, v: LoopT | int) -> bool:
226
+ """
227
+ Args:
228
+ u (LoopT | int): The loop or id of the first loop in the edge.
229
+ v (LoopT | int): The loop or id of the second loop in the edge.
230
+
231
+ Returns:
232
+ bool: True if the graph has an edge from loop u to v. False, otherwise.
233
+ """
234
+ if not self.has_loop(u) or not self.has_loop(v):
235
+ return False
236
+ if isinstance(u, int):
237
+ u = self.get_loop(u)
238
+ if isinstance(v, int):
239
+ v = self.get_loop(v)
240
+ return bool(self._loop_graph.has_edge(u, v))
241
+
242
+ def get_edge(self, u: LoopT | int, v: LoopT | int) -> EdgeT:
243
+ """
244
+
245
+ Args:
246
+ u (LoopT | int): The loop or id of the first loop in the edge.
247
+ v (LoopT | int): The loop or id of the second loop in the edge.
248
+
249
+ Returns:
250
+ EdgeT: The data about the edge from loop u to v.
251
+ """
252
+ return cast(EdgeT, self._loop_graph.edges[u, v][self._DATA_ATTRIBUTE_NAME])
253
+
254
+ def add_loop(self, loop: LoopT) -> None:
255
+ """Add the given loop to the loop graph.
256
+ Args:
257
+ loop (LoopT): The loop to add to the graph.
258
+ """
259
+ self._loop_graph.add_node(loop)
260
+ self._loops_by_loop_id[loop.loop_id] = loop
261
+
262
+ def remove_loop(self, loop: LoopT | int) -> None:
263
+ """
264
+ Remove the given loop from the graph.
265
+ Args:
266
+ loop (LoopT | int): The loop or id of the loop to remove.
267
+
268
+ Raises:
269
+ KeyError: The given loop does not exist in the graph.
270
+ """
271
+ if isinstance(loop, int):
272
+ if loop not in self._loops_by_loop_id:
273
+ raise KeyError(f"No loop with id {loop} in graph")
274
+ loop = self._loops_by_loop_id[loop]
275
+ if not self._loop_graph.has_node(loop):
276
+ raise KeyError(f"No loop {loop} in graph")
277
+ self._loop_graph.remove_node(loop)
278
+ del self._loops_by_loop_id[loop.loop_id]
279
+
280
+ def add_edge(self, u: LoopT | int, v: LoopT | int, edge_data: EdgeT) -> None:
281
+ """
282
+ Connect the given loop u to the loop v with the given edge data.
283
+ Args:
284
+ u (LoopT | int): The loop to connect to.
285
+ v (LoopT | int): The loop to connect to.
286
+ edge_data (EdgeT): The edge data to associate with the connection.
287
+
288
+ Raises:
289
+ KeyError: Either of the given loops does not exist in the graph.
290
+ """
291
+ if u not in self:
292
+ raise KeyError(f"parent loop {u} is not in graph")
293
+ if v not in self:
294
+ raise KeyError(f"child loop {v} i not in graph")
295
+ if isinstance(u, int):
296
+ u = self.get_loop(u)
297
+ if isinstance(v, int):
298
+ v = self.get_loop(v)
299
+ self._loop_graph.add_edge(u, v, data=edge_data)
300
+
301
+ def remove_edge(self, u: LoopT | int, v: LoopT | int) -> None:
302
+ """
303
+ Removes the edge from loop u to the loop v from the graph.
304
+ Args:
305
+ u (LoopT | int): The loop to connect to.
306
+ v (LoopT | int): The loop to connect to.
307
+
308
+ Raises:
309
+ KeyError: The given edge does not exist in the graph.
310
+ """
311
+ if (u, v) not in self:
312
+ raise KeyError(f"Edge from {u} to {v} is not in graph")
313
+ if isinstance(u, int):
314
+ u = self.get_loop(u)
315
+ if isinstance(v, int):
316
+ v = self.get_loop(v)
317
+ self._loop_graph.remove_edge(u, v)
318
+
319
+ def __contains__(self, item: LoopT | int | tuple[LoopT | int, LoopT | int]) -> bool:
320
+ """
321
+ Args:
322
+ item (LoopT | int | tuple[LoopT | int, LoopT | int]): The loop, loop-id, or a pair of loops in a directed edge to search for.
323
+
324
+ Returns:
325
+ bool: True if the graph contains the given loop or edge.
326
+ """
327
+ if isinstance(item, tuple):
328
+ return self.has_edge(item[0], item[1])
329
+ else:
330
+ return self.has_loop(item)
331
+
332
+ @overload
333
+ def __getitem__(self, item: int) -> LoopT: ...
334
+
335
+ @overload
336
+ def __getitem__(self, item: tuple[LoopT | int, LoopT | int]) -> EdgeT: ...
337
+
338
+ def __getitem__(self, item: int | tuple[LoopT | int, LoopT | int]) -> LoopT | EdgeT:
339
+ """
340
+ Args:
341
+ item (int | tuple[LoopT | int, LoopT | int]): The id of the loop or the pair of loops that form an edge in the graph.
342
+
343
+ Returns:
344
+ LoopT: The loop associated with the given id.
345
+ EdgeT: The edge data associated with the given pair of loops/ loop_ids.
346
+
347
+ Raises:
348
+ KeyError: The given loop or edge does not exist in the graph.
349
+ """
350
+ if item not in self:
351
+ raise KeyError(f"{item} is not in graph")
352
+ elif isinstance(item, int):
353
+ return self.get_loop(item)
354
+ else:
355
+ return self.get_edge(item[0], item[1])
356
+
357
+ def __iter__(self) -> Iterator[LoopT]:
358
+ """
359
+ Returns:
360
+ Iterator[LoopT]: Iterator over all the loops in the graph.
361
+
362
+ Notes:
363
+ No guarantees about order of loops. Expected to be insertion order.
364
+ """
365
+ return iter(self._loops_by_loop_id.values())
366
+
367
+ def __len__(self) -> int:
368
+ """
369
+ Returns:
370
+ int: The number of loops in the graph.
371
+ """
372
+ return len(self._loops_by_loop_id)
373
+
374
+
375
+ @dataclass
376
+ class Stitch_Edge:
377
+ """Common data about stitch edges."""
378
+
379
+ pull_direction: Pull_Direction # The direction of the stitch edge.
380
+
381
+
382
+ @dataclass
383
+ class Float_Edge(Generic[LoopT]):
384
+ """The edge data for float edges between loops on a yarn."""
385
+
386
+ front_loops: set[LoopT] = field(
387
+ default_factory=set
388
+ ) # The set of loops that sit in front of this float. Defaults to empty set.
389
+ back_loops: set[LoopT] = field(
390
+ default_factory=set
391
+ ) # THe set of loops that sit behind this float. Defaults to the empty set.
392
+
393
+ def loop_in_front_of_float(self, loop: LoopT) -> bool:
394
+ """
395
+ Args:
396
+ loop (LoopT): The loop to find relative to this float.
397
+
398
+ Returns:
399
+ bool: True if the loop is in the front of the float.
400
+ """
401
+ return loop in self.front_loops
402
+
403
+ def loop_behind_float(self, loop: LoopT) -> bool:
404
+ """
405
+ Args:
406
+ loop (LoopT): The loop to find relative to this float.
407
+
408
+ Returns:
409
+ bool: True if the loop is behind the float.
410
+ """
411
+ return loop in self.front_loops
412
+
413
+ def add_loop_in_front_of_float(self, loop: LoopT) -> None:
414
+ """
415
+ Adds the given loop to the set of loops in front of this float.
416
+ If the loop was behind the float, it is swapped to be in front.
417
+ Args:
418
+ loop (LoopT): The loop to put in front of this float.
419
+ """
420
+ if loop in self.back_loops:
421
+ self.back_loops.remove(loop)
422
+ self.front_loops.add(loop)
423
+
424
+ def add_loop_behind_float(self, loop: LoopT) -> None:
425
+ """
426
+ Adds the given loop to the set of loops behind this float.
427
+ If the loop was in front of this float, it is swapped to be behind.
428
+ Args:
429
+ loop (LoopT): The loop to put behind this float.
430
+ """
431
+ if loop in self.front_loops:
432
+ self.front_loops.remove(loop)
433
+ self.back_loops.add(loop)
434
+
435
+ def remove_loop_relative_to_floats(self, loop: LoopT) -> None:
436
+ """
437
+ Removes the given loop from the edge data (if present), noting that it is neither in front of nor behind the float.
438
+ Args:
439
+ loop (LoopT): The loop to remove.
440
+ """
441
+ if self.loop_in_front_of_float(loop):
442
+ self.front_loops.remove(loop)
443
+ elif self.loop_behind_float(loop):
444
+ self.back_loops.remove(loop)
445
+
446
+ def __contains__(self, item: LoopT) -> bool:
447
+ """
448
+ Args:
449
+ item (LoopT): The loop to find relative to this float.
450
+
451
+ Returns:
452
+ bool: True if the loop is in front or behind this float.
453
+ """
454
+ return item in self.front_loops or item in self.back_loops
@@ -0,0 +1,187 @@
1
+ """Module containing the Knit_Graph_Builder class."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any, Generic, TypeVar, cast
5
+
6
+ from knit_graphs.artin_wale_braids.Crossing_Direction import Crossing_Direction
7
+ from knit_graphs.Knit_Graph import Knit_Graph
8
+ from knit_graphs.Loop import Loop
9
+ from knit_graphs.Pull_Direction import Pull_Direction
10
+ from knit_graphs.Yarn import Yarn, Yarn_Properties
11
+
12
+ LoopT = TypeVar("LoopT", bound=Loop)
13
+
14
+
15
+ class Knit_Graph_Builder(Generic[LoopT]):
16
+ """A general framework for building Knit_Graphs from standard knitting operations."""
17
+
18
+ def __init__(
19
+ self,
20
+ loop_class: type[LoopT] | None = None,
21
+ yarn_class: type[Yarn[LoopT]] | None = None,
22
+ ):
23
+ self._loop_class: type[LoopT] = loop_class if loop_class is not None else cast(type[LoopT], Loop)
24
+ self._yarn_class: type[Yarn[LoopT]] = yarn_class if yarn_class is not None else cast(type[Yarn[LoopT]], Yarn)
25
+ self.knit_graph: Knit_Graph[LoopT] = Knit_Graph[LoopT]()
26
+ self._first_yarn: Yarn[LoopT] | None = None
27
+
28
+ def add_yarn(self, yarn_properties: Yarn_Properties | None = None, **yarn_kwargs: Any) -> Yarn[LoopT]:
29
+ """
30
+ Create and add a new yarn to the Knit_Graph with the given properties.
31
+ Args:
32
+ yarn_properties (Yarn_Properties, optional): The properties of the yarn. Defaults to standard yarn-properties.
33
+
34
+ Returns:
35
+ Yarn[LoopT]: The new yarn in the Knit_Graph.
36
+ """
37
+ yarn = self._new_yarn(yarn_properties, **yarn_kwargs)
38
+ self.knit_graph.add_yarn(yarn)
39
+ return yarn
40
+
41
+ def cut_yarn(self, yarn: Yarn[LoopT]) -> Yarn[LoopT]:
42
+ """Cut yarn to make it no longer active and create a new yarn instance of the same type.
43
+
44
+ Returns:
45
+ Yarn[LoopT]: New yarn of the same type after cutting this yarn.
46
+ """
47
+ cut_yarn = yarn.cut_yarn()
48
+ self.knit_graph.add_yarn(cut_yarn)
49
+ return cut_yarn
50
+
51
+ @staticmethod
52
+ def position_float(
53
+ first_loop: LoopT,
54
+ loops_behind_float: Sequence[LoopT] | None = None,
55
+ loops_in_front_of_float: Sequence[LoopT] | None = None,
56
+ ) -> None:
57
+ """
58
+ Position the float exiting the given loop relative to the given sets of loops.
59
+ Args:
60
+ first_loop (LoopT): First loop in the float to position.
61
+ loops_behind_float (Sequence[LoopT]): The loops in front of the float to position.
62
+ loops_in_front_of_float (Sequence[LoopT]): The loops behind the float to position.
63
+ """
64
+ if loops_behind_float is not None:
65
+ for loop in loops_behind_float:
66
+ first_loop.yarn.add_loop_behind_float(loop, first_loop)
67
+ if loops_in_front_of_float is not None:
68
+ for loop in loops_in_front_of_float:
69
+ first_loop.yarn.add_loop_in_front_of_float(loop, first_loop)
70
+
71
+ def tuck(self, yarn: Yarn[LoopT]) -> LoopT:
72
+ """
73
+ Forms a new loop at the end of the yarn with no parent loops.
74
+ Args:
75
+ yarn (Yarn[LoopT]): The yarn to form the loop from in the knitgraph.
76
+
77
+ Returns:
78
+ LoopT: The new loop formed by the tuck.
79
+ """
80
+ return self._add_loop(yarn)
81
+
82
+ def knit(
83
+ self, yarn: Yarn[LoopT], parent_loops: Sequence[LoopT], pull_direction: Pull_Direction | None = None
84
+ ) -> LoopT:
85
+ """
86
+ Args:
87
+ yarn (Yarn[LoopT]): The yarn to form the loop from in the knitgraph.
88
+ parent_loops (Sequence[LoopT]): The parent loops of stitch in their stacking order.
89
+ pull_direction (Pull_Direction, optional): Pull direction of the stitch. Defaults to the dominant pull direction of a loop or Back-to-front (i.e., knit-stitch) if the loop has no parents.
90
+ Returns:
91
+ LoopT: The new loop formed by the knit.
92
+ """
93
+ if pull_direction is None:
94
+ pull_direction = Loop.majority_pull_direction(parent_loops)
95
+ loop = self._add_loop(yarn)
96
+ for parent in parent_loops:
97
+ self.knit_graph.connect_loops(parent, loop, pull_direction)
98
+ return loop
99
+
100
+ def xfer(
101
+ self,
102
+ loop: LoopT,
103
+ over_loops_to_right: Sequence[LoopT] | None = None,
104
+ over_loops_to_left: Sequence[LoopT] | None = None,
105
+ under_loops_to_right: Sequence[LoopT] | None = None,
106
+ under_loops_to_left: Sequence[LoopT] | None = None,
107
+ ) -> None:
108
+ """
109
+ Cross the given loop over neighboring loops in the braid graph.
110
+ Args:
111
+ loop (LoopT): The loop to cross over neighbors
112
+ over_loops_to_right (Sequence[LoopT], optional): The loops to cross over to the right. Defaults to no loops.
113
+ over_loops_to_left (Sequence[LoopT], optional): The loops to cross over to the left. Defaults to no loops.
114
+ under_loops_to_right (Sequence[LoopT], optional): The loops to cross under to the right. Defaults to no loops.
115
+ under_loops_to_left (Sequence[LoopT], optional): The loops to cross under to the left. Defaults to no loops.
116
+ """
117
+ if over_loops_to_right is not None:
118
+ for right_loop in over_loops_to_right:
119
+ self.knit_graph.braid_graph.add_crossing(loop, right_loop, Crossing_Direction.Over_Right)
120
+ if under_loops_to_right is not None:
121
+ for right_loop in under_loops_to_right:
122
+ self.knit_graph.braid_graph.add_crossing(loop, right_loop, Crossing_Direction.Under_Right)
123
+ if over_loops_to_left is not None:
124
+ for left_loop in over_loops_to_left:
125
+ self.knit_graph.braid_graph.add_crossing(left_loop, loop, Crossing_Direction.Under_Right)
126
+ if under_loops_to_left is not None:
127
+ for left_loop in under_loops_to_left:
128
+ self.knit_graph.braid_graph.add_crossing(left_loop, loop, Crossing_Direction.Over_Right)
129
+
130
+ def split(
131
+ self,
132
+ yarn: Yarn[LoopT],
133
+ parent_loops: Sequence[LoopT],
134
+ pull_direction: Pull_Direction | None = None,
135
+ over_loops_to_right: Sequence[LoopT] | None = None,
136
+ over_loops_to_left: Sequence[LoopT] | None = None,
137
+ under_loops_to_right: Sequence[LoopT] | None = None,
138
+ under_loops_to_left: Sequence[LoopT] | None = None,
139
+ ) -> LoopT:
140
+ """
141
+
142
+ Args:
143
+ yarn (Yarn[LoopT]): The yarn to form the loop from in the knitgraph.
144
+ parent_loops (Sequence[LoopT]): The parent loops of stitch in their stacking order.
145
+ pull_direction (Pull_Direction, optional): Pull direction of the stitch. Defaults to the dominant pull direction of a loop or Back-to-front (i.e., knit-stitch) if the loop has no parents.
146
+ over_loops_to_right (Sequence[LoopT], optional): The loops to cross over to the right. Defaults to no loops.
147
+ over_loops_to_left (Sequence[LoopT], optional): The loops to cross over to the left. Defaults to no loops.
148
+ under_loops_to_right (Sequence[LoopT], optional): The loops to cross under to the right. Defaults to no loops.
149
+ under_loops_to_left (Sequence[LoopT], optional): The loops to cross under to the left. Defaults to no loops.
150
+
151
+ Returns:
152
+ LoopT: The loop formed in the split.
153
+ """
154
+ if pull_direction is None:
155
+ pull_direction = Loop.majority_pull_direction(parent_loops)
156
+ for loop in parent_loops:
157
+ self.xfer(loop, over_loops_to_right, over_loops_to_left, under_loops_to_right, under_loops_to_left)
158
+ return self.knit(yarn, parent_loops, pull_direction)
159
+
160
+ def _new_yarn(self, yarn_properties: Yarn_Properties | None = None, **yarn_kwargs: Any) -> Yarn[LoopT]:
161
+ """
162
+ Args:
163
+ yarn_properties (Yarn_Properties, optional): The properties of the yarn. Defaults to standard yarn-properties.
164
+ **yarn_kwargs (Any, optional): Additional keyword arguments for forming the yarn. Defaults to no keyword arguments.
165
+
166
+ Returns:
167
+ Yarn[LoopT]: The new yarn in the Knit_Graph.
168
+ """
169
+ if self._first_yarn is None:
170
+ self._first_yarn = self._yarn_class(yarn_properties, self.knit_graph, loop_class=self._loop_class)
171
+ return self._first_yarn
172
+ else:
173
+ return self._first_yarn.__class__(yarn_properties, self.knit_graph, loop_class=self._loop_class)
174
+
175
+ def _add_loop(self, yarn: Yarn[LoopT]) -> LoopT:
176
+ """
177
+ Create a loop at the end of the given yarn and add it to the Knit_Graph.
178
+ Args:
179
+ yarn (Yarn[LoopT]): The yarn to form the loop from in the knitgraph.
180
+
181
+ Returns:
182
+ LoopT: The new loop in the Knit_Graph.
183
+ """
184
+ loop = yarn.make_loop_on_end()
185
+ if yarn.knit_graph is not self.knit_graph:
186
+ self.knit_graph.add_loop(loop)
187
+ return loop
File without changes
@@ -0,0 +1,30 @@
1
+ """Module containing the base class for KnitGraphErrors"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from knit_graphs.Yarn import Yarn
9
+
10
+
11
+ class KnitGraphError(Exception):
12
+ """Base class for KnitGraphErrors"""
13
+
14
+ def __init__(self, message: str = "") -> None:
15
+ super().__init__(f"{self.__class__.__name__}:\n\t{message}")
16
+
17
+
18
+ class Use_Cut_Yarn_ValueError(KnitGraphError, ValueError):
19
+ """Exception for attempting to use yarn that has been cut from its carrier.
20
+ This exception occurs when trying to perform knitting operations with yarn that has been severed from its carrier,
21
+ making it impossible to continue yarn operations as the yarn is no longer connected to the carrier system."""
22
+
23
+ def __init__(self, yarn: Yarn) -> None:
24
+ """Initialize a cut yarn usage exception.
25
+
26
+ Args:
27
+ yarn (Yarn): The cut yarn that was used.
28
+ """
29
+ self.yarn: Yarn = yarn
30
+ super().__init__(f"Cannot create more loops on a cut yarn: {self.yarn}")
knit_graphs/py.typed ADDED
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: knit-graphs
3
- Version: 0.0.10
3
+ Version: 0.0.12
4
4
  Summary: A graph representation of knitted structures where each loop is a node and edges represent yarn and stitch relationships.
5
5
  Home-page: https://mhofmann-khoury.github.io/knit_graph/
6
6
  License: MIT
@@ -20,8 +20,9 @@ Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Typing :: Typed
23
24
  Requires-Dist: networkx (>=3.5)
24
- Requires-Dist: plotly (>=6.3.0,<7.0.0)
25
+ Requires-Dist: plotly (>=6.3.0)
25
26
  Project-URL: Documentation, https://mhofmann-khoury.github.io/knit_graph/
26
27
  Project-URL: Repository, https://github.com/mhofmann-Khoury/knit_graph/
27
28
  Description-Content-Type: text/markdown