knit-graphs 0.0.11__tar.gz → 0.0.12__tar.gz

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.
Files changed (47) hide show
  1. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/PKG-INFO +1 -1
  2. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/pyproject.toml +1 -1
  3. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/Course.py +63 -42
  4. knit_graphs-0.0.12/src/knit_graphs/Knit_Graph.py +209 -0
  5. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/Knit_Graph_Visualizer.py +51 -63
  6. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/Loop.py +122 -116
  7. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/Pull_Direction.py +3 -2
  8. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/Yarn.py +246 -233
  9. knit_graphs-0.0.12/src/knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +96 -0
  10. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/Wale.py +61 -82
  11. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/Wale_Braid.py +7 -2
  12. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/Wale_Braid_Word.py +35 -22
  13. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/Wale_Group.py +46 -39
  14. knit_graphs-0.0.12/src/knit_graphs/basic_knit_graph_generators.py +207 -0
  15. knit_graphs-0.0.12/src/knit_graphs/directed_loop_graph.py +454 -0
  16. knit_graphs-0.0.12/src/knit_graphs/knit_graph_builder.py +187 -0
  17. knit_graphs-0.0.12/src/knit_graphs/knit_graph_errors/knit_graph_error.py +30 -0
  18. knit_graphs-0.0.12/src/knit_graphs/py.typed +0 -0
  19. knit_graphs-0.0.11/src/knit_graphs/Knit_Graph.py +0 -307
  20. knit_graphs-0.0.11/src/knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +0 -116
  21. knit_graphs-0.0.11/src/knit_graphs/basic_knit_graph_generators.py +0 -286
  22. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/LICENSE +0 -0
  23. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/README.md +0 -0
  24. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/Makefile +0 -0
  25. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/make.bat +0 -0
  26. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Course.rst +0 -0
  27. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Knit_Graph.rst +0 -0
  28. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Knit_Graph_Visualizer.rst +0 -0
  29. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Loop.rst +0 -0
  30. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Pull_Direction.rst +0 -0
  31. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.Yarn.rst +0 -0
  32. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Crossing_Direction.rst +0 -0
  33. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Loop_Braid_Graph.rst +0 -0
  34. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Wale.rst +0 -0
  35. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Braid.rst +0 -0
  36. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Braid_Word.rst +0 -0
  37. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Group.rst +0 -0
  38. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.artin_wale_braids.rst +0 -0
  39. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.basic_knit_graph_generators.rst +0 -0
  40. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/api/knit_graphs.rst +0 -0
  41. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/conf.py +0 -0
  42. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/index.rst +0 -0
  43. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/docs/source/installation.rst +0 -0
  44. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/__init__.py +0 -0
  45. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/Crossing_Direction.py +0 -0
  46. {knit_graphs-0.0.11 → knit_graphs-0.0.12}/src/knit_graphs/artin_wale_braids/__init__.py +0 -0
  47. /knit_graphs-0.0.11/src/knit_graphs/py.typed → /knit_graphs-0.0.12/src/knit_graphs/knit_graph_errors/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: knit-graphs
3
- Version: 0.0.11
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
@@ -12,7 +12,7 @@ build-backend = "poetry.core.masonry.api" # Use Poetry's build system
12
12
  # All the information about your project that will appear on PyPI
13
13
  [tool.poetry]
14
14
  name = "knit-graphs"
15
- version = "0.0.11"
15
+ version = "0.0.12"
16
16
  description = "A graph representation of knitted structures where each loop is a node and edges represent yarn and stitch relationships."
17
17
  authors = ["Megan Hofmann <m.hofmann@northeastern.edu>"]
18
18
  maintainers = ["Megan Hofmann <m.hofmann@northeastern.edu>"]
@@ -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.
@@ -0,0 +1,209 @@
1
+ """The graph structure used to represent knitted objects.
2
+
3
+ This module contains the main Knit_Graph class which serves as the central data structure for representing knitted fabrics.
4
+ It manages the relationships between loops, yarns, and structural elements like courses and wales.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterator
10
+ from typing import TypeVar
11
+
12
+ from knit_graphs.artin_wale_braids.Crossing_Direction import Crossing_Direction
13
+ from knit_graphs.artin_wale_braids.Loop_Braid_Graph import Loop_Braid_Graph
14
+ from knit_graphs.artin_wale_braids.Wale import Wale
15
+ from knit_graphs.artin_wale_braids.Wale_Group import Wale_Group
16
+ from knit_graphs.Course import Course
17
+ from knit_graphs.directed_loop_graph import Directed_Loop_Graph
18
+ from knit_graphs.Loop import Loop
19
+ from knit_graphs.Pull_Direction import Pull_Direction
20
+ from knit_graphs.Yarn import Yarn
21
+
22
+ LoopT = TypeVar("LoopT", bound=Loop)
23
+
24
+
25
+ class Knit_Graph(Directed_Loop_Graph[LoopT, Pull_Direction]):
26
+ """A representation of knitted structures as connections between loops on yarns.
27
+
28
+ The Knit_Graph class is the main data structure for representing knitted fabrics.
29
+ It maintains a directed graph of loops connected by stitch edges, manages yarn relationships,
30
+ and provides methods for analyzing the structure of the knitted fabric including courses, wales, and cable crossings.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ """Initialize an empty knit graph with no loops or yarns."""
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()
39
+
40
+ @property
41
+ def last_loop(self) -> LoopT | None:
42
+ """Get the most recently added loop in the graph.
43
+
44
+ Returns:
45
+ Loop | None: The last loop added to the graph, or None if no loops have been added.
46
+ """
47
+ return self._last_loop
48
+
49
+ @property
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.
66
+
67
+ Returns:
68
+ Pull_Direction: The pull direction of the stitch-edge between the parent and child.
69
+ """
70
+ return self.get_edge(parent, child)
71
+
72
+ def add_crossing(self, left_loop: LoopT, right_loop: LoopT, crossing_direction: Crossing_Direction) -> None:
73
+ """Add a cable crossing between two loops with the specified crossing direction.
74
+
75
+ Args:
76
+ left_loop (Loop): The loop on the left side of the crossing.
77
+ right_loop (Loop): The loop on the right side of the crossing.
78
+ crossing_direction (Crossing_Direction): The direction of the crossing (over, under, or none) between the loops.
79
+ """
80
+ self.braid_graph.add_crossing(left_loop, right_loop, crossing_direction)
81
+
82
+ def add_loop(self, loop: LoopT) -> None:
83
+ """Add a loop to the knit graph as a node.
84
+
85
+ Args:
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.
87
+ """
88
+ super().add_loop(loop)
89
+ if loop.yarn not in self.yarns:
90
+ self.add_yarn(loop.yarn)
91
+ if self._last_loop is None or loop > self._last_loop:
92
+ self._last_loop = loop
93
+
94
+ def remove_loop(self, loop: LoopT | int) -> None:
95
+ """
96
+ Remove the given loop from the knit graph.
97
+ Args:
98
+ loop (Loop | int): The loop or loop_id to be removed.
99
+
100
+ Raises:
101
+ KeyError: If the loop is not in the knit graph.
102
+
103
+ """
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.
108
+ # Remove any stitch edges involving this loop.
109
+ if self.has_child_loop(loop):
110
+ child_loop = self.get_child_loop(loop)
111
+ if child_loop is not None:
112
+ child_loop.remove_parent(loop)
113
+ super().remove_loop(loop)
114
+ # Remove loop from any floating positions
115
+ for yarn in self.yarns:
116
+ yarn.remove_loop_relative_to_floats(loop)
117
+ # Remove loop from yarn
118
+ yarn = loop.yarn
119
+ yarn.remove_loop(loop)
120
+ if len(yarn) == 0: # This was the only loop on that yarn
121
+ self.yarns.discard(yarn)
122
+ # Reset last loop
123
+ if loop is self.last_loop:
124
+ if len(self.yarns) == 0: # No loops left
125
+ self._last_loop = None
126
+ else: # Set to the newest loop formed at the end of any yarns.
127
+ self._last_loop = max(y.last_loop for y in self.yarns if y.last_loop is not None)
128
+
129
+ def add_yarn(self, yarn: Yarn[LoopT]) -> None:
130
+ """Add a yarn to the graph without adding its loops.
131
+
132
+ Args:
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.
134
+ """
135
+ self.yarns.add(yarn)
136
+
137
+ def connect_loops(
138
+ self,
139
+ parent_loop: LoopT,
140
+ child_loop: LoopT,
141
+ pull_direction: Pull_Direction = Pull_Direction.BtF,
142
+ stack_position: int | None = None,
143
+ ) -> None:
144
+ """Create a stitch edge by connecting a parent and child loop.
145
+
146
+ Args:
147
+ parent_loop (Loop): The parent loop to connect to the child loop.
148
+ child_loop (Loop): The child loop to connect to the parent loop.
149
+ pull_direction (Pull_Direction): The direction the child is pulled through the parent. Defaults to Pull_Direction.BtF (knit stitch).
150
+ stack_position (int | None, optional): The position to insert the parent into the child's parent stack. If None, adds on top of the stack. Defaults to None.
151
+
152
+ Raises:
153
+ KeyError: If either the parent_loop or child_loop is not already in the knit graph.
154
+ """
155
+ super().add_edge(parent_loop, child_loop, pull_direction)
156
+ child_loop.add_parent_loop(parent_loop, stack_position)
157
+
158
+ def get_wales_ending_with_loop(self, last_loop: LoopT) -> set[Wale[LoopT]]:
159
+ """Get all wales (vertical columns of stitches) that end at the specified loop.
160
+
161
+ Args:
162
+ last_loop (Loop): The last loop of the joined set of wales.
163
+
164
+ Returns:
165
+ set[Wale[LoopT]]: The set of wales that end at this loop.
166
+ """
167
+ if len(last_loop.parent_loops) == 0:
168
+ return {Wale[LoopT](last_loop, self)}
169
+ sources = self.source_loops(last_loop)
170
+ return {Wale[LoopT](source, self) for source in sources}
171
+
172
+ def get_terminal_wales(self) -> dict[LoopT, list[Wale]]:
173
+ """
174
+ Get wale groups organized by their terminal loops.
175
+
176
+ Returns:
177
+ dict[Loop, list[Wale]]: Dictionary mapping terminal loops to list of wales that terminate that wale.
178
+ """
179
+ wale_groups = {}
180
+ for loop in self.terminal_loops:
181
+ wale_groups[loop] = list(self.get_wales_ending_with_loop(loop))
182
+ return wale_groups
183
+
184
+ def get_courses(self) -> list[Course[LoopT]]:
185
+ """Get all courses (horizontal rows) in the knit graph in chronological order.
186
+
187
+ Returns:
188
+ list[Course[LoopT]: A list of courses representing horizontal rows of loops.
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.
190
+ """
191
+ courses = []
192
+ course = Course(0, self)
193
+ for loop in self.sorted_loops:
194
+ for parent in loop.parent_loops:
195
+ if parent in course: # start a new course
196
+ courses.append(course)
197
+ course = Course(course.course_number + 1, self)
198
+ break
199
+ course.add_loop(loop)
200
+ courses.append(course)
201
+ return courses
202
+
203
+ def get_wale_groups(self) -> set[Wale_Group]:
204
+ """Get wale groups organized by their terminal loops.
205
+
206
+ Returns:
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.
208
+ """
209
+ return {Wale_Group(terminal_loop, self) for terminal_loop in self.terminal_loops}