knit-graphs 0.0.9__py3-none-any.whl → 0.0.11__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.
knit_graphs/Loop.py CHANGED
@@ -3,9 +3,10 @@
3
3
  This module defines the Loop class which represents individual loops in a knitting pattern.
4
4
  Loops are the fundamental building blocks of knitted structures and maintain relationships with parent loops, yarn connections, and float positions.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
8
- from typing import TYPE_CHECKING, cast
9
+ from typing import TYPE_CHECKING
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from knit_graphs.Yarn import Yarn
@@ -47,7 +48,12 @@ class Loop:
47
48
  Args:
48
49
  u (Loop): The first loop in the float pair.
49
50
  v (Loop): The second loop in the float pair.
51
+
52
+ Raises:
53
+ ValueError: If u and v are not on the same yarn.
50
54
  """
55
+ if u.yarn != v.yarn:
56
+ raise ValueError("Loops of a float must share a yarn.")
51
57
  if u not in self.back_floats:
52
58
  self.back_floats[u] = set()
53
59
  if v not in self.back_floats:
@@ -55,6 +61,18 @@ class Loop:
55
61
  self.back_floats[u].add(v)
56
62
  self.back_floats[v].add(u)
57
63
 
64
+ def remove_loop_from_front_floats(self) -> None:
65
+ """
66
+ Removes this loop from being in front of all marked floats. Mutates the yarns that own edges of those floats.
67
+ """
68
+ visited: set[Loop] = set()
69
+ for u, v_loops in self.front_floats.items():
70
+ visited.add(u)
71
+ for v in v_loops:
72
+ if v not in visited and v in u.yarn: # float shares a yarn
73
+ u.yarn.loop_graph.edges[u, v]["Back_Loops"].remove(self)
74
+ self.front_floats = {}
75
+
58
76
  def add_loop_behind_float(self, u: Loop, v: Loop) -> None:
59
77
  """Set this loop to be behind the float between loops u and v.
60
78
 
@@ -63,7 +81,12 @@ class Loop:
63
81
  Args:
64
82
  u (Loop): The first loop in the float pair.
65
83
  v (Loop): The second loop in the float pair.
84
+
85
+ Raises:
86
+ ValueError: If u and v are not on the same yarn.
66
87
  """
88
+ if u.yarn != v.yarn:
89
+ raise ValueError("Loops of a float must share a yarn.")
67
90
  if u not in self.front_floats:
68
91
  self.front_floats[u] = set()
69
92
  if v not in self.front_floats:
@@ -71,6 +94,18 @@ class Loop:
71
94
  self.front_floats[u].add(v)
72
95
  self.front_floats[v].add(u)
73
96
 
97
+ def remove_loop_from_back_floats(self) -> None:
98
+ """
99
+ Removes this loop from being behind of all marked floats. Mutates the yarns that own edges of those floats.
100
+ """
101
+ visited = set()
102
+ for u, v_loops in self.back_floats.items():
103
+ visited.add(u)
104
+ for v in v_loops:
105
+ if v not in visited and v in u.yarn: # float shares a yarn
106
+ u.yarn.loop_graph.edges[u, v]["Front_Loops"].remove(self)
107
+ self.back_floats = {}
108
+
74
109
  def is_in_front_of_float(self, u: Loop, v: Loop) -> bool:
75
110
  """Check if this loop is positioned in front of the float between loops u and v.
76
111
 
@@ -101,19 +136,25 @@ class Loop:
101
136
  Returns:
102
137
  Loop | None: The prior loop on the yarn, or None if this is the first loop on the yarn.
103
138
  """
104
- loop = self.yarn.prior_loop(self)
105
- if loop is None:
106
- return None
107
- else:
108
- return loop
139
+ return self.yarn.prior_loop(self)
109
140
 
110
- def next_loop_on_yarn(self) -> Loop:
141
+ def next_loop_on_yarn(self) -> Loop | None:
111
142
  """Get the loop that follows this loop on the same yarn.
112
143
 
113
144
  Returns:
114
- Loop: The next loop on the yarn, or None if this is the last loop on the yarn.
145
+ Loop | None: The next loop on the yarn, or None if this is the last loop on the yarn.
146
+ """
147
+ return self.yarn.next_loop(self)
148
+
149
+ def remove_parent_loops(self) -> list[Loop]:
150
+ """
151
+ Removes the list of parent loops from this loop.
152
+ Returns:
153
+ list[Loop]: The list of parent loops that were removed.
115
154
  """
116
- return cast(Loop, self.yarn.next_loop(self))
155
+ parents = self.parent_loops
156
+ self.parent_loops = []
157
+ return parents
117
158
 
118
159
  def has_parent_loops(self) -> bool:
119
160
  """Check if this loop has any parent loops connected through stitch edges.
@@ -135,6 +176,31 @@ class Loop:
135
176
  else:
136
177
  self.parent_loops.append(parent)
137
178
 
179
+ def remove_parent(self, parent: Loop) -> None:
180
+ """
181
+ Removes the given parent loop from the set of parents of this loop.
182
+ If the given loop is not a parent of this loop, nothing happens.
183
+ Args:
184
+ parent (Loop): The parent loop to remove.
185
+ """
186
+ if parent in self.parent_loops:
187
+ self.parent_loops.remove(parent)
188
+
189
+ def ancestor_loops(self) -> set[Loop]:
190
+ """
191
+ Returns:
192
+ set[Loop]: The set of loops that initiate all wales that lead to this loop. The empty set if this loop has no parents.
193
+ """
194
+ if not self.has_parent_loops():
195
+ return set()
196
+ ancestors = set()
197
+ for parent_loop in self.parent_loops:
198
+ if parent_loop.has_parent_loops():
199
+ ancestors.update(parent_loop.ancestor_loops())
200
+ else:
201
+ ancestors.add(parent_loop)
202
+ return ancestors
203
+
138
204
  @property
139
205
  def loop_id(self) -> int:
140
206
  """Get the unique identifier of this loop.
@@ -160,7 +226,7 @@ class Loop:
160
226
  """
161
227
  return self.loop_id
162
228
 
163
- def __eq__(self, other: Loop) -> bool:
229
+ def __eq__(self, other: object) -> bool:
164
230
  """Check equality with another base loop based on loop_id and type.
165
231
 
166
232
  Args:
@@ -169,7 +235,7 @@ class Loop:
169
235
  Returns:
170
236
  bool: True if both loops have the same class and loop_id, False otherwise.
171
237
  """
172
- return isinstance(other, other.__class__) and self.loop_id == other.loop_id
238
+ return isinstance(other, Loop) and isinstance(other, other.__class__) and self.loop_id == other.loop_id
173
239
 
174
240
  def __lt__(self, other: Loop | int) -> bool:
175
241
  """Compare loop_id with another loop or integer for ordering.
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module defines the Pull_Direction enumeration which represents the two ways a loop can be pulled through other loops in knitting: from back to front (knit) or from front to back (purl).
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from enum import Enum
@@ -13,6 +14,7 @@ class Pull_Direction(Enum):
13
14
  This enumeration represents the two directions that yarn can be pulled through loops to create different stitch types.
14
15
  BtF (Back to Front) creates knit stitches, while FtB (Front to Back) creates purl stitches.
15
16
  """
17
+
16
18
  BtF = "Knit"
17
19
  FtB = "Purl"
18
20
 
knit_graphs/Yarn.py CHANGED
@@ -3,10 +3,12 @@
3
3
  This module contains the Yarn class and Yarn_Properties dataclass which together represent the physical yarn used in knitting patterns.
4
4
  The Yarn class manages the sequence of loops along a yarn and their floating relationships.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
9
+ from collections.abc import Iterator
8
10
  from dataclasses import dataclass
9
- from typing import TYPE_CHECKING, Iterator, cast
11
+ from typing import TYPE_CHECKING, cast
10
12
 
11
13
  from networkx import DiGraph, dfs_edges, dfs_preorder_nodes
12
14
 
@@ -22,6 +24,7 @@ class Yarn_Properties:
22
24
 
23
25
  This frozen dataclass contains all the physical and visual properties that characterize a yarn, including its structure, weight, and appearance.
24
26
  """
27
+
25
28
  name: str = "yarn" # name (str): The name or identifier for this yarn type.
26
29
  plies: int = 2 # plies (int): The number of individual strands twisted together to form the yarn.
27
30
  weight: float = 28 # weight (float): The weight category or thickness of the yarn.
@@ -52,7 +55,7 @@ class Yarn_Properties:
52
55
  """
53
56
  return Yarn_Properties()
54
57
 
55
- def __eq__(self, other: Yarn_Properties) -> bool:
58
+ def __eq__(self, other: object) -> bool:
56
59
  """Check equality with another Yarn_Properties instance.
57
60
 
58
61
  Args:
@@ -61,7 +64,13 @@ class Yarn_Properties:
61
64
  Returns:
62
65
  bool: True if all properties (name, plies, weight, color) are equal, False otherwise.
63
66
  """
64
- return self.name == other.name and self.plies == other.plies and self.weight == other.weight and self.color == other.color
67
+ return (
68
+ isinstance(other, Yarn_Properties)
69
+ and self.name == other.name
70
+ and self.plies == other.plies
71
+ and self.weight == other.weight
72
+ and self.color == other.color
73
+ )
65
74
 
66
75
  def __hash__(self) -> int:
67
76
  """Get hash value for use in sets and dictionaries.
@@ -83,7 +92,14 @@ class Yarn:
83
92
  properties (Yarn_Properties): The physical and visual properties of this yarn.
84
93
  """
85
94
 
86
- def __init__(self, yarn_properties: None | Yarn_Properties = None, knit_graph: None | Knit_Graph = None):
95
+ FRONT_LOOPS: str = "Front_Loops"
96
+ _BACK_LOOPS: str = "Back_Loops"
97
+
98
+ def __init__(
99
+ self,
100
+ yarn_properties: None | Yarn_Properties = None,
101
+ knit_graph: None | Knit_Graph = None,
102
+ ):
87
103
  """Initialize a yarn with the specified properties and optional knit graph association.
88
104
 
89
105
  Args:
@@ -141,7 +157,7 @@ class Yarn:
141
157
  self.add_loop_in_front_of_float(front_loop, v, u)
142
158
  else:
143
159
  return
144
- self.loop_graph.edges[u, v]["Front_Loops"].add(front_loop)
160
+ self.loop_graph.edges[u, v][self.FRONT_LOOPS].add(front_loop)
145
161
  front_loop.add_loop_in_front_of_float(u, v)
146
162
 
147
163
  def add_loop_behind_float(self, back_loop: Loop, u: Loop, v: Loop) -> None:
@@ -157,7 +173,7 @@ class Yarn:
157
173
  self.add_loop_behind_float(back_loop, v, u)
158
174
  else:
159
175
  return
160
- self.loop_graph.edges[u, v]["Back_Loops"].add(back_loop)
176
+ self.loop_graph.edges[u, v][self._BACK_LOOPS].add(back_loop)
161
177
  back_loop.add_loop_behind_float(u, v)
162
178
 
163
179
  def get_loops_in_front_of_float(self, u: Loop, v: Loop) -> set[Loop]:
@@ -176,7 +192,7 @@ class Yarn:
176
192
  else:
177
193
  return set()
178
194
  else:
179
- return cast(set[Loop], self.loop_graph.edges[u, v]['Front_Loops'])
195
+ return cast(set[Loop], self.loop_graph.edges[u, v][self.FRONT_LOOPS])
180
196
 
181
197
  def get_loops_behind_float(self, u: Loop, v: Loop) -> set[Loop]:
182
198
  """Get all loops positioned behind the float between two loops.
@@ -194,7 +210,7 @@ class Yarn:
194
210
  else:
195
211
  return set()
196
212
  else:
197
- return cast(set[Loop], self.loop_graph.edges[u, v]['Back_Loops'])
213
+ return cast(set[Loop], self.loop_graph.edges[u, v][self._BACK_LOOPS])
198
214
 
199
215
  @property
200
216
  def last_loop(self) -> Loop | None:
@@ -290,6 +306,46 @@ class Yarn:
290
306
  else:
291
307
  return self.last_loop.loop_id + 1
292
308
 
309
+ def remove_loop(self, loop: Loop) -> None:
310
+ """
311
+ Remove the given loop from the yarn.
312
+ Reconnects any neighboring loops to form a new float with the positioned in-front-of or behind the original floats positioned accordingly.
313
+ Resets the first_loop and last_loop properties if the removed loop was the tail of the yarn.
314
+ Args:
315
+ loop (Loop): The loop to remove from the yarn.
316
+
317
+ Raises:
318
+ KeyError: The given loop does not exist in the yarn.
319
+ """
320
+ if loop not in self:
321
+ raise KeyError(f"Loop {loop} does not exist on yarn {self}.")
322
+ prior_loop = self.prior_loop(loop)
323
+ next_loop = self.next_loop(loop)
324
+ if isinstance(prior_loop, Loop) and isinstance(next_loop, Loop): # Loop is between two floats to be merged.
325
+ front_of_float_loops = self.get_loops_in_front_of_float(prior_loop, loop)
326
+ front_of_float_loops.update(self.get_loops_in_front_of_float(loop, next_loop))
327
+ back_of_float_loops = self.get_loops_behind_float(prior_loop, loop)
328
+ back_of_float_loops.update(self.get_loops_behind_float(loop, next_loop))
329
+ self.loop_graph.remove_node(loop)
330
+ self.loop_graph.add_edge(
331
+ prior_loop,
332
+ next_loop,
333
+ Front_Loops=front_of_float_loops,
334
+ Back_Loops=back_of_float_loops,
335
+ )
336
+ for front_loop in front_of_float_loops:
337
+ front_loop.add_loop_in_front_of_float(prior_loop, next_loop)
338
+ for back_loop in back_of_float_loops:
339
+ back_loop.add_loop_behind_float(prior_loop, next_loop)
340
+ return
341
+ if next_loop is None: # This was the last loop, make the prior loop the last loop.
342
+ assert loop is self.last_loop
343
+ self._last_loop = prior_loop
344
+ if prior_loop is None: # This was the first loop, make the next loop the first loop.
345
+ assert loop is self.first_loop
346
+ self._first_loop = next_loop
347
+ self.loop_graph.remove_node(loop)
348
+
293
349
  def add_loop_to_end(self, loop: Loop) -> Loop:
294
350
  """Add an existing loop to the end of this yarn and associated knit graph.
295
351
 
@@ -396,8 +452,11 @@ class Yarn:
396
452
  list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned in front of that float.
397
453
  Only includes floats that have loops in front of them.
398
454
  """
399
- return [(u, v, self.get_loops_in_front_of_float(u, v)) for u, v in self.edge_iter()
400
- if len(self.get_loops_in_front_of_float(u, v)) > 0]
455
+ return [
456
+ (u, v, self.get_loops_in_front_of_float(u, v))
457
+ for u, v in self.edge_iter()
458
+ if len(self.get_loops_in_front_of_float(u, v)) > 0
459
+ ]
401
460
 
402
461
  def loops_behind_floats(self) -> list[tuple[Loop, Loop, set[Loop]]]:
403
462
  """Get all float segments with loops positioned behind them.
@@ -406,8 +465,11 @@ class Yarn:
406
465
  list[tuple[Loop, Loop, set[Loop]]]: List of tuples containing the two loops defining each float and the set of loops positioned behind that float.
407
466
  Only includes floats that have loops behind them.
408
467
  """
409
- return [(u, v, self.get_loops_behind_float(u, v)) for u, v in self.edge_iter()
410
- if len(self.get_loops_behind_float(u, v)) > 0]
468
+ return [
469
+ (u, v, self.get_loops_behind_float(u, v))
470
+ for u, v in self.edge_iter()
471
+ if len(self.get_loops_behind_float(u, v)) > 0
472
+ ]
411
473
 
412
474
  def __getitem__(self, item: int) -> Loop:
413
475
  """Get a loop by its ID from this yarn.
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module defines the Crossing_Direction enumeration which represents the different ways loops can cross over or under each other in cable knitting patterns.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from enum import Enum
@@ -13,6 +14,7 @@ class Crossing_Direction(Enum):
13
14
  This enumeration represents the three possible crossing relationships between loops: crossing over to the right, crossing under to the right, or no crossing at all.
14
15
  These directions are fundamental to representing cable structures in knitted fabrics.
15
16
  """
17
+
16
18
  Over_Right = "+"
17
19
  Under_Right = "-"
18
20
  No_Cross = "|"
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module provides the Loop_Braid_Graph class which tracks crossing relationships between loops in cable knitting patterns using a directed graph structure.
4
4
  """
5
+
5
6
  from typing import cast
6
7
 
7
8
  from networkx import DiGraph
@@ -20,6 +21,8 @@ class Loop_Braid_Graph:
20
21
  loop_crossing_graph (DiGraph): A NetworkX directed graph storing loop crossing relationships with crossing direction attributes.
21
22
  """
22
23
 
24
+ _CROSSING = "crossing"
25
+
23
26
  def __init__(self) -> None:
24
27
  """Initialize an empty loop braid graph with no crossings."""
25
28
  self.loop_crossing_graph: DiGraph = DiGraph()
@@ -34,6 +37,15 @@ class Loop_Braid_Graph:
34
37
  """
35
38
  self.loop_crossing_graph.add_edge(left_loop, right_loop, crossing=crossing_direction)
36
39
 
40
+ def remove_loop(self, loop: Loop) -> None:
41
+ """
42
+ Removes any crossings that involve the given loop.
43
+ Args:
44
+ loop (Loop): The loop to remove.
45
+ """
46
+ if loop in self:
47
+ self.loop_crossing_graph.remove_node(loop)
48
+
37
49
  def __contains__(self, item: Loop | tuple[Loop, Loop]) -> bool:
38
50
  """Check if a loop or loop pair is contained in the braid graph.
39
51
 
@@ -60,8 +72,11 @@ class Loop_Braid_Graph:
60
72
  if left_loop not in self:
61
73
  return []
62
74
  else:
63
- return [rl for rl in self.loop_crossing_graph.successors(left_loop)
64
- if self.get_crossing(left_loop, rl) is not Crossing_Direction.No_Cross]
75
+ return [
76
+ rl
77
+ for rl in self.loop_crossing_graph.successors(left_loop)
78
+ if self.get_crossing(left_loop, rl) is not Crossing_Direction.No_Cross
79
+ ]
65
80
 
66
81
  def right_crossing_loops(self, right_loop: Loop) -> list[Loop]:
67
82
  """Get all loops that cross with the given loop when it is on the right side.
@@ -75,8 +90,11 @@ class Loop_Braid_Graph:
75
90
  if right_loop not in self:
76
91
  return []
77
92
  else:
78
- return [l for l in self.loop_crossing_graph.predecessors(right_loop)
79
- if self.get_crossing(l, right_loop) is not Crossing_Direction.No_Cross]
93
+ return [
94
+ l
95
+ for l in self.loop_crossing_graph.predecessors(right_loop)
96
+ if self.get_crossing(l, right_loop) is not Crossing_Direction.No_Cross
97
+ ]
80
98
 
81
99
  def get_crossing(self, left_loop: Loop, right_loop: Loop) -> Crossing_Direction:
82
100
  """Get the crossing direction between two loops, creating a no-cross edge if none exists.
@@ -92,4 +110,7 @@ class Loop_Braid_Graph:
92
110
  """
93
111
  if not self.loop_crossing_graph.has_edge(left_loop, right_loop):
94
112
  self.add_crossing(left_loop, right_loop, Crossing_Direction.No_Cross)
95
- return cast(Crossing_Direction, self.loop_crossing_graph[left_loop][right_loop]['crossing'])
113
+ return cast(
114
+ Crossing_Direction,
115
+ self.loop_crossing_graph[left_loop][right_loop][self._CROSSING],
116
+ )
@@ -2,15 +2,20 @@
2
2
 
3
3
  This module defines the Wale class which represents a vertical column of stitches in a knitted structure, maintaining the sequence and relationships between loops in that column.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
- from typing import Iterator, cast
8
+ from collections.abc import Iterator
9
+ from typing import TYPE_CHECKING, cast
8
10
 
9
11
  from networkx import DiGraph, dfs_preorder_nodes
10
12
 
11
13
  from knit_graphs.Loop import Loop
12
14
  from knit_graphs.Pull_Direction import Pull_Direction
13
15
 
16
+ if TYPE_CHECKING:
17
+ from knit_graphs.Knit_Graph import Knit_Graph
18
+
14
19
 
15
20
  class Wale:
16
21
  """A data structure representing stitch relationships between loops in a vertical column of a knitted structure.
@@ -24,48 +29,46 @@ class Wale:
24
29
  stitches (DiGraph): Stores the directed graph of stitch connections within this wale.
25
30
  """
26
31
 
27
- def __init__(self, first_loop: Loop | None = None) -> None:
32
+ _PULL_DIRECTION: str = "pull_direction"
33
+
34
+ def __init__(self, first_loop: Loop, knit_graph: Knit_Graph, end_loop: Loop | None = None) -> None:
28
35
  """Initialize a wale optionally starting with a specified loop.
29
36
 
30
37
  Args:
31
- first_loop (Loop | None, optional): The initial loop to start the wale with. If provided, it will be added as both the first and last loop. Defaults to None.
38
+ first_loop (Loop): The initial loop to start the wale with.
39
+ knit_graph (Knit_Graph): The knit graph that owns this wale.
40
+ end_loop (Loop, optional):
41
+ The loop to terminate the wale with.
42
+ If no loop is provided or this loop is not found, the wale will terminate at the first loop with no child.
32
43
  """
33
44
  self.stitches: DiGraph = DiGraph()
34
- self.first_loop: None | Loop = first_loop
35
- self.last_loop: None | Loop = None
36
- if isinstance(self.first_loop, Loop):
37
- self.add_loop_to_end(self.first_loop, pull_direction=None)
38
-
39
- def add_loop_to_end(self, loop: Loop, pull_direction: Pull_Direction | None = Pull_Direction.BtF) -> None:
40
- """Add a loop to the end (top) of the wale with the specified pull direction.
41
-
42
- Args:
43
- loop (Loop): The loop to add to the end of the wale.
44
- pull_direction (Pull_Direction | None, optional): The direction to pull the loop through its parent loop. Defaults to Pull_Direction.BtF. Can be None only for the first loop in the wale.
45
+ self.stitches.add_node(first_loop)
46
+ self._knit_graph: Knit_Graph = knit_graph
47
+ self.first_loop: Loop = first_loop
48
+ self.last_loop: Loop = first_loop
49
+ self._build_wale_from_first_loop(end_loop)
50
+
51
+ def _build_wale_from_first_loop(self, end_loop: Loop | None) -> None:
52
+ while self._knit_graph.has_child_loop(self.last_loop):
53
+ child = self._knit_graph.get_child_loop(self.last_loop)
54
+ assert isinstance(child, Loop)
55
+ self.add_loop_to_end(child)
56
+ if end_loop is not None and child is end_loop:
57
+ return # found the end loop, so wrap up the wale
58
+
59
+ def add_loop_to_end(self, loop: Loop) -> None:
45
60
  """
46
- if self.last_loop is None:
47
- self.stitches.add_node(loop)
48
- self.first_loop = loop
49
- self.last_loop = loop
50
- else:
51
- assert isinstance(pull_direction, Pull_Direction)
52
- self.stitches.add_edge(self.last_loop, loop, pull_direction=pull_direction)
53
- self.last_loop = loop
54
-
55
- def add_loop_to_beginning(self, loop: Loop, pull_direction: Pull_Direction = Pull_Direction.BtF) -> None:
56
- """Add a loop to the beginning (bottom) of the wale with the specified pull direction.
61
+ Add a loop to the end (top) of the wale with the specified pull direction.
57
62
 
58
63
  Args:
59
- loop (Loop): The loop to add to the beginning of the wale.
60
- pull_direction (Pull_Direction, optional): The direction to pull the existing first loop through this new loop. Defaults to Pull_Direction.BtF.
64
+ loop (Loop): The loop to add to the end of the wale.
61
65
  """
62
- if self.first_loop is None:
63
- self.stitches.add_node(loop)
64
- self.first_loop = loop
65
- self.last_loop = loop
66
- else:
67
- self.stitches.add_edge(loop, self.first_loop, pull_direction=pull_direction)
68
- self.first_loop = loop
66
+ self.stitches.add_edge(
67
+ self.last_loop,
68
+ loop,
69
+ pull_direction=self._knit_graph.get_pull_direction(self.last_loop, loop),
70
+ )
71
+ self.last_loop = loop
69
72
 
70
73
  def get_stitch_pull_direction(self, u: Loop, v: Loop) -> Pull_Direction:
71
74
  """Get the pull direction of the stitch edge between two loops in this wale.
@@ -77,10 +80,11 @@ class Wale:
77
80
  Returns:
78
81
  Pull_Direction: The pull direction of the stitch between loops u and v.
79
82
  """
80
- return cast(Pull_Direction, self.stitches.edges[u, v]["pull_direction"])
83
+ return cast(Pull_Direction, self.stitches.edges[u, v][self._PULL_DIRECTION])
81
84
 
82
85
  def split_wale(self, split_loop: Loop) -> tuple[Wale, Wale | None]:
83
- """Split this wale at the specified loop into two separate wales.
86
+ """
87
+ Split this wale at the specified loop into two separate wales.
84
88
 
85
89
  The split loop becomes the last loop of the first wale and the first loop of the second wale.
86
90
 
@@ -93,19 +97,25 @@ class Wale:
93
97
  * The first wale (from start to split_loop). This will be the whole wale if the split_loop is not found.
94
98
  * The second wale (from split_loop to end). This will be None if the split_loop is not found.
95
99
  """
96
- first_wale = Wale(self.first_loop)
97
- growing_wale = first_wale
98
- found_loop = False
99
- for l in cast(list[Loop], self[1:]):
100
- if l is split_loop:
101
- growing_wale.add_loop_to_end(l, self.get_stitch_pull_direction(cast(Loop, growing_wale.last_loop), l))
102
- growing_wale = Wale(split_loop)
103
- found_loop = True
104
- else:
105
- growing_wale.add_loop_to_end(l, self.get_stitch_pull_direction(cast(Loop, growing_wale.last_loop), l))
106
- if not found_loop:
100
+ if split_loop in self:
101
+ return (
102
+ Wale(self.first_loop, self._knit_graph, end_loop=split_loop),
103
+ Wale(split_loop, self._knit_graph, end_loop=self.last_loop),
104
+ )
105
+ else:
107
106
  return self, None
108
- return first_wale, growing_wale
107
+
108
+ def __eq__(self, other: object) -> bool:
109
+ """
110
+ Args:
111
+ other (Wale): The wale to compare.
112
+
113
+ Returns:
114
+ bool: True if all the loops in both wales are present and in the same order. False, otherwise.
115
+ """
116
+ if not isinstance(other, Wale) or len(self) != len(other):
117
+ return False
118
+ return not any(l != o for l, o in zip(self, other, strict=False))
109
119
 
110
120
  def __len__(self) -> int:
111
121
  """Get the number of loops in this wale.
@@ -149,13 +159,28 @@ class Wale:
149
159
  return bool(self.stitches.has_node(item))
150
160
 
151
161
  def __hash__(self) -> int:
152
- """Get the hash value of this wale based on its first loop.
162
+ """
163
+ Get the hash value of this wale based on its first loop.
153
164
 
154
165
  Returns:
155
166
  int: Hash value based on the first loop in this wale.
156
167
  """
157
168
  return hash(self.first_loop)
158
169
 
170
+ def __str__(self) -> str:
171
+ """
172
+ Returns:
173
+ str: The string representation of this wale.
174
+ """
175
+ return f"Wale({self.first_loop}->{self.last_loop})"
176
+
177
+ def __repr__(self) -> str:
178
+ """
179
+ Returns:
180
+ str: The string representation of this wale.
181
+ """
182
+ return str(self)
183
+
159
184
  def overlaps(self, other: Wale) -> bool:
160
185
  """Check if this wale has any loops in common with another wale.
161
186
 
@@ -3,6 +3,7 @@
3
3
  This module provides the Wale_Braid class which represents complex cable knitting patterns as mathematical braid structures,
4
4
  using concepts from algebraic topology to model how wales cross over and under each other.
5
5
  """
6
+
6
7
  from knit_graphs.artin_wale_braids.Wale_Braid_Word import Wale_Braid_Word
7
8
  from knit_graphs.artin_wale_braids.Wale_Group import Wale_Group
8
9
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  This module provides the Wale_Braid_Word class which represents a single step in a braid operation, describing how loops cross over each other within a course of knitting.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from knit_graphs.artin_wale_braids.Crossing_Direction import Crossing_Direction
@@ -59,7 +60,7 @@ class Wale_Braid_Word:
59
60
  new_crossings = {i: ~c for i, c in self.crossings.items()}
60
61
  return Wale_Braid_Word(new_loops, new_crossings)
61
62
 
62
- def __eq__(self, other: Wale_Braid_Word) -> bool:
63
+ def __eq__(self, other: object) -> bool:
63
64
  """Check equality with another wale braid word.
64
65
 
65
66
  Two braid words are equal if they have the same loops in the same order and the same crossing operations at the same indices.
@@ -70,13 +71,13 @@ class Wale_Braid_Word:
70
71
  Returns:
71
72
  bool: True if both braid words have identical loops, loop ordering, and crossing operations, False otherwise.
72
73
  """
73
- if (len(self) != len(other)
74
- or any(l != o for l, o in zip(self.loops, other.loops))
75
- or any(i not in other.crossings for i in self.crossings)
76
- or any(other.crossings[i] != cd for i, cd in self.crossings.items())):
77
- return False
78
- else:
79
- return True
74
+ return (
75
+ isinstance(other, Wale_Braid_Word)
76
+ and len(self) == len(other)
77
+ and all(l == o for l, o in zip(self.loops, other.loops, strict=False))
78
+ and all(i in other.crossings for i in self.crossings)
79
+ and all(other.crossings[i] == cd for i, cd in self.crossings.items())
80
+ )
80
81
 
81
82
  def is_inversion(self, other: Wale_Braid_Word) -> bool:
82
83
  """Check if another braid word is the inverse of this braid word.