knit-graphs 0.0.11__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.
knit_graphs/Course.py CHANGED
@@ -5,34 +5,45 @@ This module contains the Course class which represents a horizontal row of loops
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from collections.abc import Iterator
9
- from typing import TYPE_CHECKING, cast
8
+ from collections.abc import Iterator, Sequence
9
+ from typing import TYPE_CHECKING, Generic, TypeVar, overload
10
10
 
11
11
  from knit_graphs.Loop import Loop
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from knit_graphs.Knit_Graph import Knit_Graph
15
15
 
16
+ LoopT = TypeVar("LoopT", bound=Loop)
16
17
 
17
- class Course:
18
+
19
+ class Course(Sequence[LoopT], Generic[LoopT]):
18
20
  """Course object for organizing loops into knitting rows.
19
21
 
20
22
  A Course represents a horizontal row of loops in a knitting pattern.
21
23
  It maintains an ordered list of loops and provides methods for analyzing the structure and relationships between courses in the knitted fabric.
22
24
  """
23
25
 
24
- def __init__(self, knit_graph: Knit_Graph) -> None:
26
+ def __init__(self, course_number: int, knit_graph: Knit_Graph[LoopT]) -> None:
25
27
  """Initialize an empty course associated with a knit graph.
26
28
 
27
29
  Args:
28
30
  knit_graph (Knit_Graph): The knit graph that this course belongs to.
29
31
  """
30
- self._knit_graph: Knit_Graph = knit_graph
31
- self._loops_in_order: list[Loop] = []
32
- self._loop_set: set[Loop] = set()
32
+ self._course_number: int = course_number
33
+ self._knit_graph: Knit_Graph[LoopT] = knit_graph
34
+ self._loops_in_order: list[LoopT] = []
35
+ self._loop_set: set[LoopT] = set()
33
36
 
34
37
  @property
35
- def loops_in_order(self) -> list[Loop]:
38
+ def course_number(self) -> int:
39
+ """
40
+ Returns:
41
+ int: The course number of the course.
42
+ """
43
+ return self._course_number
44
+
45
+ @property
46
+ def loops_in_order(self) -> list[LoopT]:
36
47
  """
37
48
  Returns:
38
49
  list[Loop]: The list of loops in this course.
@@ -40,22 +51,34 @@ class Course:
40
51
  return self._loops_in_order
41
52
 
42
53
  @property
43
- def knit_graph(self) -> Knit_Graph:
54
+ def loop_ids_in_course(self) -> list[int]:
55
+ """
56
+ Returns:
57
+ list[int]: The loop ids in the course in the order the occur in the course.
58
+ """
59
+ return [l.loop_id for l in self.loops_in_order]
60
+
61
+ @property
62
+ def knit_graph(self) -> Knit_Graph[LoopT]:
44
63
  """
45
64
  Returns:
46
65
  Knit_Graph: The knit graph that this course belongs to.
47
66
  """
48
67
  return self._knit_graph
49
68
 
50
- def add_loop(self, loop: Loop, index: int | None = None) -> None:
69
+ def add_loop(self, loop: LoopT, index: int | None = None) -> None:
51
70
  """Add a loop to the course at the specified index or at the end.
52
71
 
53
72
  Args:
54
73
  loop (Loop): The loop to add to this course.
55
74
  index (int | None, optional): The index position to insert the loop at. If None, appends to the end.
75
+
76
+ Raises:
77
+ ValueError: If the loop is a parent to any loops already in the course.
56
78
  """
57
79
  for parent_loop in loop.parent_loops:
58
- assert parent_loop not in self, f"{loop} has parent {parent_loop}, cannot be added to same course"
80
+ if parent_loop in self:
81
+ raise ValueError(f"{loop} has parent {parent_loop}, cannot be added to same course")
59
82
  self._loop_set.add(loop)
60
83
  if index is None:
61
84
  self.loops_in_order.append(loop)
@@ -68,7 +91,7 @@ class Course:
68
91
  Returns:
69
92
  bool: True if the course has at least one yarn over (loop with no parent loops) to start new wales.
70
93
  """
71
- return any(not loop.has_parent_loops() for loop in self)
94
+ return any(not loop.has_parent_loops for loop in self)
72
95
 
73
96
  def has_decrease(self) -> bool:
74
97
  """Check if this course contains any decrease stitches that merge wales.
@@ -78,18 +101,7 @@ class Course:
78
101
  """
79
102
  return any(len(loop.parent_loops) > 1 for loop in self)
80
103
 
81
- def __getitem__(self, index: int | slice) -> Loop | list[Loop]:
82
- """Get loop(s) at the specified index or slice.
83
-
84
- Args:
85
- index (int | slice): The index or slice to retrieve from the course.
86
-
87
- Returns:
88
- Loop | list[Loop]: The loop at the specified index, or list of loops for a slice.
89
- """
90
- return self.loops_in_order[index]
91
-
92
- def in_round_with(self, next_course: Course) -> bool:
104
+ def in_round_with(self, next_course: Course[LoopT]) -> bool:
93
105
  """Check if the next course connects to this course in a circular pattern.
94
106
 
95
107
  This method determines if the courses are connected in the round (circular knitting) by checking if the next course starts at the beginning of this course.
@@ -100,14 +112,14 @@ class Course:
100
112
  Returns:
101
113
  bool: True if the next course starts at the beginning of this course, indicating circular knitting.
102
114
  """
103
- next_start: Loop = cast(Loop, next_course[0])
115
+ next_start = next_course[0]
104
116
  i = 1
105
- while not next_start.has_parent_loops():
106
- next_start = cast(Loop, next_course[i])
117
+ while not next_start.has_parent_loops:
118
+ next_start = next_course[i]
107
119
  i += 1
108
120
  return self[0] in next_start.parent_loops
109
121
 
110
- def in_row_with(self, next_course: Course) -> bool:
122
+ def in_row_with(self, next_course: Course[LoopT]) -> bool:
111
123
  """Check if the next course connects to this course in a flat/row pattern.
112
124
 
113
125
  This method determines if the courses are connected in flat knitting (back and forth) by checking if the next course starts at the end of this course.
@@ -118,39 +130,48 @@ class Course:
118
130
  Returns:
119
131
  bool: True if the next course starts at the end of this course, indicating flat/row knitting.
120
132
  """
121
- next_start: Loop = cast(Loop, next_course[0])
133
+ next_start: LoopT = next_course[0]
122
134
  i = 1
123
- while not next_start.has_parent_loops():
124
- next_start = cast(Loop, next_course[i])
135
+ while not next_start.has_parent_loops:
136
+ next_start = next_course[i]
125
137
  i += 1
126
138
  return self[-1] in next_start.parent_loops
127
139
 
128
- def __contains__(self, loop: Loop) -> bool:
140
+ def __contains__(self, loop: object) -> bool:
129
141
  """Check if a loop is contained in this course.
130
142
 
131
143
  Args:
132
- loop (Loop): The loop to check for membership in this course.
144
+ loop (LoopT): The loop to check for membership in this course.
133
145
 
134
146
  Returns:
135
147
  bool: True if the loop is in this course, False otherwise.
136
148
  """
137
149
  return loop in self._loop_set
138
150
 
139
- def __iter__(self) -> Iterator[Loop]:
140
- """Iterate over loops in this course in order.
151
+ @overload
152
+ def __getitem__(self, index: int) -> LoopT: ...
153
+
154
+ @overload
155
+ def __getitem__(self, index: slice) -> list[LoopT]: ...
156
+
157
+ def __getitem__(self, index: int | slice) -> LoopT | list[LoopT]:
158
+ """Get loop(s) at the specified index or slice.
159
+
160
+ Args:
161
+ index (int | slice): The index or slice to retrieve from the course.
141
162
 
142
163
  Returns:
143
- Iterator[Loop]: An iterator over the loops in this course in their natural order.
164
+ Loop | list[Loop]: The loop at the specified index, or list of loops for a slice.
144
165
  """
145
- return iter(self.loops_in_order)
166
+ return self.loops_in_order[index]
146
167
 
147
- def __reversed__(self) -> Iterator[Loop]:
148
- """Iterate over loops in this course in reverse order.
168
+ def __iter__(self) -> Iterator[LoopT]:
169
+ """Iterate over loops in this course in order.
149
170
 
150
171
  Returns:
151
- Iterator[Loop]: An iterator over the loops in this course in reverse order.
172
+ Iterator[Loop]: An iterator over the loops in this course in their natural order.
152
173
  """
153
- return reversed(self.loops_in_order)
174
+ return iter(self.loops_in_order)
154
175
 
155
176
  def __len__(self) -> int:
156
177
  """Get the number of loops in this course.
@@ -166,7 +187,7 @@ class Course:
166
187
  Returns:
167
188
  str: String representation showing the ordered list of loops.
168
189
  """
169
- return str(self.loops_in_order)
190
+ return f"Course {self.course_number}: {self.loops_in_order}"
170
191
 
171
192
  def __repr__(self) -> str:
172
193
  """Get string representation of this course for debugging.
knit_graphs/Knit_Graph.py CHANGED
@@ -7,21 +7,22 @@ It manages the relationships between loops, yarns, and structural elements like
7
7
  from __future__ import annotations
8
8
 
9
9
  from collections.abc import Iterator
10
- from typing import Any, cast
11
-
12
- from networkx import DiGraph
10
+ from typing import TypeVar
13
11
 
14
12
  from knit_graphs.artin_wale_braids.Crossing_Direction import Crossing_Direction
15
13
  from knit_graphs.artin_wale_braids.Loop_Braid_Graph import Loop_Braid_Graph
16
14
  from knit_graphs.artin_wale_braids.Wale import Wale
17
15
  from knit_graphs.artin_wale_braids.Wale_Group import Wale_Group
18
16
  from knit_graphs.Course import Course
17
+ from knit_graphs.directed_loop_graph import Directed_Loop_Graph
19
18
  from knit_graphs.Loop import Loop
20
19
  from knit_graphs.Pull_Direction import Pull_Direction
21
20
  from knit_graphs.Yarn import Yarn
22
21
 
22
+ LoopT = TypeVar("LoopT", bound=Loop)
23
+
23
24
 
24
- class Knit_Graph:
25
+ class Knit_Graph(Directed_Loop_Graph[LoopT, Pull_Direction]):
25
26
  """A representation of knitted structures as connections between loops on yarns.
26
27
 
27
28
  The Knit_Graph class is the main data structure for representing knitted fabrics.
@@ -31,30 +32,44 @@ class Knit_Graph:
31
32
 
32
33
  def __init__(self) -> None:
33
34
  """Initialize an empty knit graph with no loops or yarns."""
34
- self.stitch_graph: DiGraph = DiGraph()
35
- self.braid_graph: Loop_Braid_Graph = Loop_Braid_Graph()
36
- self._last_loop: None | Loop = None
37
- self.yarns: set[Yarn] = set()
35
+ super().__init__()
36
+ self.braid_graph: Loop_Braid_Graph[LoopT] = Loop_Braid_Graph()
37
+ self._last_loop: LoopT | None = None
38
+ self.yarns: set[Yarn[LoopT]] = set()
38
39
 
39
40
  @property
40
- def last_loop(self) -> None | Loop:
41
+ def last_loop(self) -> LoopT | None:
41
42
  """Get the most recently added loop in the graph.
42
43
 
43
44
  Returns:
44
- None | Loop: The last loop added to the graph, or None if no loops have been added.
45
+ Loop | None: The last loop added to the graph, or None if no loops have been added.
45
46
  """
46
47
  return self._last_loop
47
48
 
48
49
  @property
49
- def has_loop(self) -> bool:
50
- """Check if the graph contains any loops.
50
+ def stitch_iter(self) -> Iterator[tuple[LoopT, LoopT, Pull_Direction]]:
51
+ """
52
+ Returns:
53
+ Iterator[tuple[LoopT, LoopT, Pull_Direction]]: Iterator over the edges and edge-data in the graph.
54
+
55
+ Notes:
56
+ No guarantees about the order of the edges.
57
+ """
58
+ return self.edge_iter
59
+
60
+ def get_pull_direction(self, parent: LoopT | int, child: LoopT | int) -> Pull_Direction:
61
+ """Get the pull direction of the stitch edge between parent and child loops.
62
+
63
+ Args:
64
+ parent (Loop | int): The parent loop of the stitch edge.
65
+ child (Loop | int): The child loop of the stitch edge.
51
66
 
52
67
  Returns:
53
- bool: True if the graph has at least one loop, False otherwise.
68
+ Pull_Direction: The pull direction of the stitch-edge between the parent and child.
54
69
  """
55
- return self.last_loop is not None
70
+ return self.get_edge(parent, child)
56
71
 
57
- def add_crossing(self, left_loop: Loop, right_loop: Loop, crossing_direction: Crossing_Direction) -> None:
72
+ def add_crossing(self, left_loop: LoopT, right_loop: LoopT, crossing_direction: Crossing_Direction) -> None:
58
73
  """Add a cable crossing between two loops with the specified crossing direction.
59
74
 
60
75
  Args:
@@ -64,41 +79,41 @@ class Knit_Graph:
64
79
  """
65
80
  self.braid_graph.add_crossing(left_loop, right_loop, crossing_direction)
66
81
 
67
- def add_loop(self, loop: Loop) -> None:
82
+ def add_loop(self, loop: LoopT) -> None:
68
83
  """Add a loop to the knit graph as a node.
69
84
 
70
85
  Args:
71
86
  loop (Loop): The loop to be added as a node in the graph. If the loop's yarn is not already in the graph, it will be added automatically.
72
87
  """
73
- self.stitch_graph.add_node(loop)
88
+ super().add_loop(loop)
74
89
  if loop.yarn not in self.yarns:
75
90
  self.add_yarn(loop.yarn)
76
91
  if self._last_loop is None or loop > self._last_loop:
77
92
  self._last_loop = loop
78
93
 
79
- def remove_loop(self, loop: Loop) -> None:
94
+ def remove_loop(self, loop: LoopT | int) -> None:
80
95
  """
81
96
  Remove the given loop from the knit graph.
82
97
  Args:
83
- loop (Loop): The loop to be removed.
98
+ loop (Loop | int): The loop or loop_id to be removed.
84
99
 
85
100
  Raises:
86
101
  KeyError: If the loop is not in the knit graph.
87
102
 
88
103
  """
89
- if loop not in self:
90
- raise KeyError(f"Loop {loop} not on the knit graph")
91
- self.braid_graph.remove_loop(loop) # remove any crossing associated with this loop.
104
+ if isinstance(loop, int):
105
+ loop = self.get_loop(loop)
106
+ if loop in self.braid_graph:
107
+ self.braid_graph.remove_loop(loop) # remove any crossing associated with this loop.
92
108
  # Remove any stitch edges involving this loop.
93
- loop.remove_parent_loops()
94
109
  if self.has_child_loop(loop):
95
110
  child_loop = self.get_child_loop(loop)
96
- assert isinstance(child_loop, Loop)
97
- child_loop.remove_parent(loop)
98
- self.stitch_graph.remove_node(loop)
111
+ if child_loop is not None:
112
+ child_loop.remove_parent(loop)
113
+ super().remove_loop(loop)
99
114
  # Remove loop from any floating positions
100
- loop.remove_loop_from_front_floats()
101
- loop.remove_loop_from_back_floats()
115
+ for yarn in self.yarns:
116
+ yarn.remove_loop_relative_to_floats(loop)
102
117
  # Remove loop from yarn
103
118
  yarn = loop.yarn
104
119
  yarn.remove_loop(loop)
@@ -107,23 +122,22 @@ class Knit_Graph:
107
122
  # Reset last loop
108
123
  if loop is self.last_loop:
109
124
  if len(self.yarns) == 0: # No loops left
110
- assert len(self.stitch_graph.nodes) == 0
111
125
  self._last_loop = None
112
126
  else: # Set to the newest loop formed at the end of any yarns.
113
- self._last_loop = max(y.last_loop for y in self.yarns if isinstance(y.last_loop, Loop))
127
+ self._last_loop = max(y.last_loop for y in self.yarns if y.last_loop is not None)
114
128
 
115
- def add_yarn(self, yarn: Yarn) -> None:
129
+ def add_yarn(self, yarn: Yarn[LoopT]) -> None:
116
130
  """Add a yarn to the graph without adding its loops.
117
131
 
118
132
  Args:
119
- yarn (Yarn): The yarn to be added to the graph structure. This method assumes that loops do not need to be added separately.
133
+ yarn (Yarn[LoopT]): The yarn to be added to the graph structure. This method assumes that loops do not need to be added separately.
120
134
  """
121
135
  self.yarns.add(yarn)
122
136
 
123
137
  def connect_loops(
124
138
  self,
125
- parent_loop: Loop,
126
- child_loop: Loop,
139
+ parent_loop: LoopT,
140
+ child_loop: LoopT,
127
141
  pull_direction: Pull_Direction = Pull_Direction.BtF,
128
142
  stack_position: int | None = None,
129
143
  ) -> None:
@@ -138,28 +152,24 @@ class Knit_Graph:
138
152
  Raises:
139
153
  KeyError: If either the parent_loop or child_loop is not already in the knit graph.
140
154
  """
141
- if parent_loop not in self:
142
- raise KeyError(f"parent loop {parent_loop} not in Knit Graph")
143
- if child_loop not in self:
144
- raise KeyError(f"child loop {parent_loop} not in Knit Graph")
145
- self.stitch_graph.add_edge(parent_loop, child_loop, pull_direction=pull_direction)
155
+ super().add_edge(parent_loop, child_loop, pull_direction)
146
156
  child_loop.add_parent_loop(parent_loop, stack_position)
147
157
 
148
- def get_wales_ending_with_loop(self, last_loop: Loop) -> set[Wale]:
158
+ def get_wales_ending_with_loop(self, last_loop: LoopT) -> set[Wale[LoopT]]:
149
159
  """Get all wales (vertical columns of stitches) that end at the specified loop.
150
160
 
151
161
  Args:
152
162
  last_loop (Loop): The last loop of the joined set of wales.
153
163
 
154
164
  Returns:
155
- set[Wale]: The set of wales that end at this loop.
165
+ set[Wale[LoopT]]: The set of wales that end at this loop.
156
166
  """
157
167
  if len(last_loop.parent_loops) == 0:
158
- return {Wale(last_loop, self)}
159
- ancestors = last_loop.ancestor_loops()
160
- return {Wale(ancestor_loop, self) for ancestor_loop in ancestors}
168
+ return {Wale[LoopT](last_loop, self)}
169
+ sources = self.source_loops(last_loop)
170
+ return {Wale[LoopT](source, self) for source in sources}
161
171
 
162
- def get_terminal_wales(self) -> dict[Loop, list[Wale]]:
172
+ def get_terminal_wales(self) -> dict[LoopT, list[Wale]]:
163
173
  """
164
174
  Get wale groups organized by their terminal loops.
165
175
 
@@ -167,24 +177,24 @@ class Knit_Graph:
167
177
  dict[Loop, list[Wale]]: Dictionary mapping terminal loops to list of wales that terminate that wale.
168
178
  """
169
179
  wale_groups = {}
170
- for loop in self.terminal_loops():
180
+ for loop in self.terminal_loops:
171
181
  wale_groups[loop] = list(self.get_wales_ending_with_loop(loop))
172
182
  return wale_groups
173
183
 
174
- def get_courses(self) -> list[Course]:
184
+ def get_courses(self) -> list[Course[LoopT]]:
175
185
  """Get all courses (horizontal rows) in the knit graph in chronological order.
176
186
 
177
187
  Returns:
178
- list[Course]: A list of courses representing horizontal rows of loops.
188
+ list[Course[LoopT]: A list of courses representing horizontal rows of loops.
179
189
  The first course contains the initial set of loops. A course change occurs when a loop has a parent loop in the previous course.
180
190
  """
181
191
  courses = []
182
- course = Course(self)
183
- for loop in self.sorted_loops():
192
+ course = Course(0, self)
193
+ for loop in self.sorted_loops:
184
194
  for parent in loop.parent_loops:
185
195
  if parent in course: # start a new course
186
196
  courses.append(course)
187
- course = Course(self)
197
+ course = Course(course.course_number + 1, self)
188
198
  break
189
199
  course.add_loop(loop)
190
200
  courses.append(course)
@@ -196,112 +206,4 @@ class Knit_Graph:
196
206
  Returns:
197
207
  set[Wale_Group]: The set of wale-groups that lead to the terminal loops of this graph. Each wale group represents a collection of wales that end at the same terminal loop.
198
208
  """
199
- return {Wale_Group(terminal_loop, self) for terminal_loop in self.terminal_loops()}
200
-
201
- def __contains__(self, item: Loop | tuple[Loop, Loop]) -> bool:
202
- """Check if a loop is contained in the knit graph.
203
-
204
- Args:
205
- item (Loop | tuple[Loop, Loop]): The loop being checked for in the graph or the parent-child stitch edge to check for in the knit graph.
206
-
207
- Returns:
208
- bool: True if the given loop or stitch edge is in the graph, False otherwise.
209
- """
210
- if isinstance(item, Loop):
211
- return bool(self.stitch_graph.has_node(item))
212
- else:
213
- return bool(self.stitch_graph.has_edge(item[0], item[1]))
214
-
215
- def __iter__(self) -> Iterator[Loop]:
216
- """
217
- Returns:
218
- Iterator[Loop]: An iterator over all loops in the knit graph.
219
- """
220
- return cast(Iterator[Loop], iter(self.stitch_graph.nodes))
221
-
222
- def __getitem__(self, item: int) -> Loop:
223
- loop = next((loop for loop in self if loop.loop_id == item), None)
224
- if loop is None:
225
- raise KeyError(f"Loop of id {item} not in knit graph")
226
- return loop
227
-
228
- def sorted_loops(self) -> list[Loop]:
229
- """
230
- Returns:
231
- list[Loop]: The list of loops in the stitch graph sorted from the earliest formed loop to the latest formed loop.
232
- """
233
- return sorted(self.stitch_graph.nodes)
234
-
235
- def get_pull_direction(self, parent: Loop, child: Loop) -> Pull_Direction | None:
236
- """Get the pull direction of the stitch edge between parent and child loops.
237
-
238
- Args:
239
- parent (Loop): The parent loop of the stitch edge.
240
- child (Loop): The child loop of the stitch edge.
241
-
242
- Returns:
243
- Pull_Direction | None: The pull direction of the stitch-edge between the parent and child, or None if there is no edge between these loops.
244
- """
245
- edge = self.get_stitch_edge(parent, child)
246
- if edge is None:
247
- return None
248
- else:
249
- return cast(Pull_Direction, edge["pull_direction"])
250
-
251
- def get_stitch_edge(self, parent: Loop, child: Loop) -> dict[str, Any] | None:
252
- """Get the stitch edge data between two loops.
253
-
254
- Args:
255
- parent (Loop): The parent loop of the stitch edge.
256
- child (Loop): The child loop of the stitch edge.
257
-
258
- Returns:
259
- dict[str, Any] | None: The edge data dictionary for this stitch edge, or None if no edge exists between these loops.
260
- """
261
- if self.stitch_graph.has_edge(parent, child):
262
- return cast(dict[str, Any], self.stitch_graph.get_edge_data(parent, child))
263
- else:
264
- return None
265
-
266
- def get_child_loop(self, loop: Loop) -> Loop | None:
267
- """Get the child loop of the specified parent loop.
268
-
269
- Args:
270
- loop (Loop): The loop to look for a child loop from.
271
-
272
- Returns:
273
- Loop | None: The child loop if one exists, or None if no child loop is found.
274
- """
275
- successors = [*self.stitch_graph.successors(loop)]
276
- if len(successors) == 0:
277
- return None
278
- return cast(Loop, successors[0])
279
-
280
- def has_child_loop(self, loop: Loop) -> bool:
281
- """Check if a loop has a child loop connected to it.
282
-
283
- Args:
284
- loop (Loop): The loop to check for child connections.
285
-
286
- Returns:
287
- bool: True if the loop has a child loop, False otherwise.
288
- """
289
- return self.get_child_loop(loop) is not None
290
-
291
- def is_terminal_loop(self, loop: Loop) -> bool:
292
- """Check if a loop is terminal (has no child loops and terminates a wale).
293
-
294
- Args:
295
- loop (Loop): The loop to check for terminal status.
296
-
297
- Returns:
298
- bool: True if the loop has no child loops and terminates a wale, False otherwise.
299
- """
300
- return not self.has_child_loop(loop)
301
-
302
- def terminal_loops(self) -> Iterator[Loop]:
303
- """
304
- Returns:
305
- Iterator[Loop]: An iterator over all terminal loops in the knit graph.
306
- """
307
- return iter(loop for loop in self if self.is_terminal_loop(loop))
209
+ return {Wale_Group(terminal_loop, self) for terminal_loop in self.terminal_loops}