knit-graphs 0.0.9__tar.gz → 0.0.10__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 (39) hide show
  1. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/PKG-INFO +1 -1
  2. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/pyproject.toml +1 -49
  3. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Knit_Graph.py +68 -39
  4. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Loop.py +74 -9
  5. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Yarn.py +41 -4
  6. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Loop_Braid_Graph.py +11 -1
  7. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Wale.py +65 -49
  8. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Wale_Group.py +75 -62
  9. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/LICENSE +0 -0
  10. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/README.md +0 -0
  11. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/Makefile +0 -0
  12. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/make.bat +0 -0
  13. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Course.rst +0 -0
  14. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Knit_Graph.rst +0 -0
  15. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Knit_Graph_Visualizer.rst +0 -0
  16. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Loop.rst +0 -0
  17. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Pull_Direction.rst +0 -0
  18. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.Yarn.rst +0 -0
  19. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Crossing_Direction.rst +0 -0
  20. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Loop_Braid_Graph.rst +0 -0
  21. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Wale.rst +0 -0
  22. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Braid.rst +0 -0
  23. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Braid_Word.rst +0 -0
  24. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.Wale_Group.rst +0 -0
  25. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.artin_wale_braids.rst +0 -0
  26. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.basic_knit_graph_generators.rst +0 -0
  27. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/api/knit_graphs.rst +0 -0
  28. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/conf.py +0 -0
  29. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/index.rst +0 -0
  30. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/docs/source/installation.rst +0 -0
  31. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Course.py +0 -0
  32. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Knit_Graph_Visualizer.py +0 -0
  33. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/Pull_Direction.py +0 -0
  34. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/__init__.py +0 -0
  35. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Crossing_Direction.py +0 -0
  36. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Wale_Braid.py +0 -0
  37. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/Wale_Braid_Word.py +0 -0
  38. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/artin_wale_braids/__init__.py +0 -0
  39. {knit_graphs-0.0.9 → knit_graphs-0.0.10}/src/knit_graphs/basic_knit_graph_generators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: knit-graphs
3
- Version: 0.0.9
3
+ Version: 0.0.10
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.9"
15
+ version = "0.0.10"
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>"]
@@ -52,8 +52,6 @@ include = [
52
52
  "README.md", # Project description
53
53
  "LICENSE", # License file
54
54
  "docs/**/*", # All documentation files
55
- # "src/**/data/**/*", # Example: include data files
56
- # "src/**/templates/**/*", # Example: include template files
57
55
  ]
58
56
 
59
57
  # Exclude files from the distribution package (keeps package size down)
@@ -80,29 +78,11 @@ exclude = [
80
78
  python = ">=3.11,<3.14"
81
79
  networkx = ">=3.5"
82
80
  plotly = "^6.3.0"
83
- # Examples:
84
- # requests = "^2.31.0" # For HTTP requests
85
- # pydantic = "^2.0.0" # For data validation
86
- # click = "^8.1.0" # For CLI applications
87
- # numpy = "^1.24.0" # For numerical computing
88
- # pandas = "^2.0.0" # For data manipulation
89
-
90
- # =============================================================================
91
- # OPTIONAL DEPENDENCIES (EXTRAS)
92
- # =============================================================================
93
- # Optional dependency groups that users can install with pip install "package[extra]"
94
- #[tool.poetry.extras]
95
- # Examples:
96
- # cli = ["click", "rich"] # For command-line interface features
97
- # viz = ["matplotlib", "plotly"] # For visualization features
98
- # dev = ["pytest", "mypy"] # For development tools (though prefer dev dependencies)
99
81
 
100
82
  # =============================================================================
101
83
  # DEVELOPMENT DEPENDENCIES
102
84
  # =============================================================================
103
85
  # These packages are only needed during development and testing
104
-
105
-
106
86
  [tool.poetry.group.dev.dependencies]
107
87
  importlib-resources = ">=6.5.2"
108
88
 
@@ -143,34 +123,6 @@ tox = "^4.11.0" # Test across multiple Python versions locall
143
123
  # -------------------------------------------------------------------------
144
124
  build = "^0.10.0" # PEP 517 build tool (for creating distributions)
145
125
 
146
- # =============================================================================
147
- # DOCUMENTATION-SPECIFIC DEPENDENCIES
148
- # =============================================================================
149
- # Separate dependency group for building documentation (allows selective installation)
150
- [tool.poetry.group.docs.dependencies]
151
- sphinx = "^7.1.0" # Documentation generator (main tool)
152
- sphinx-rtd-theme = "^1.3.0" # Professional-looking theme
153
- sphinx-autodoc-typehints = "^1.24.0" # Automatically include type hints in docs
154
- sphinx-autoapi = "^3.0.0" # Automatically generates API documentation
155
- myst-parser = "^2.0.0" # Support for Markdown files in documentation
156
-
157
- # =============================================================================
158
- # COMMAND LINE SCRIPTS
159
- # =============================================================================
160
- # Define command-line entry points for your package
161
- #[tool.poetry.scripts]
162
- # Example:
163
- # your-command = "your_project_name.cli:main" # Creates 'your-command' executable
164
-
165
- # =============================================================================
166
- # PROJECT URLS FOR PYPI
167
- # =============================================================================
168
- # Additional URLs that will be displayed on your PyPI project page
169
- #[tool.poetry.urls]
170
- #"Bug Tracker" = "https://github.com/mhofmann-Khoury/knit-graphs/issues"
171
- #"Changelog" = "https://github.com/mhofmann-Khoury/knit-graphs/blob/main/CHANGELOG.md"
172
- #"Discussions" = "https://github.com/your-username/your-project-name/discussions"
173
-
174
126
  # =============================================================================
175
127
  # ISORT IMPORT SORTER CONFIGURATION
176
128
  # =============================================================================
@@ -18,9 +18,6 @@ from knit_graphs.Loop import Loop
18
18
  from knit_graphs.Pull_Direction import Pull_Direction
19
19
  from knit_graphs.Yarn import Yarn
20
20
 
21
- # from knit_graphs.artin_wale_braids.Wale import Wale
22
- # from knit_graphs.artin_wale_braids.Wale_Group import Wale_Group
23
-
24
21
 
25
22
  class Knit_Graph:
26
23
  """A representation of knitted structures as connections between loops on yarns.
@@ -77,6 +74,42 @@ class Knit_Graph:
77
74
  if self._last_loop is None or loop > self._last_loop:
78
75
  self._last_loop = loop
79
76
 
77
+ def remove_loop(self, loop: Loop) -> None:
78
+ """
79
+ Remove the given loop from the knit graph.
80
+ Args:
81
+ loop (Loop): The loop to be removed.
82
+
83
+ Raises:
84
+ KeyError: If the loop is not in the knit graph.
85
+
86
+ """
87
+ if loop not in self:
88
+ raise KeyError(f"Loop {loop} not on the knit graph")
89
+ self.braid_graph.remove_loop(loop) # remove any crossing associated with this loop.
90
+ # Remove any stitch edges involving this loop.
91
+ loop.remove_parent_loops()
92
+ if self.has_child_loop(loop):
93
+ child_loop = self.get_child_loop(loop)
94
+ assert isinstance(child_loop, Loop)
95
+ child_loop.remove_parent(loop)
96
+ self.stitch_graph.remove_node(loop)
97
+ # Remove loop from any floating positions
98
+ loop.remove_loop_from_front_floats()
99
+ loop.remove_loop_from_back_floats()
100
+ # Remove loop from yarn
101
+ yarn = loop.yarn
102
+ yarn.remove_loop(loop)
103
+ if len(yarn) == 0: # This was the only loop on that yarn
104
+ self.yarns.discard(yarn)
105
+ # Reset last loop
106
+ if loop is self.last_loop:
107
+ if len(self.yarns) == 0: # No loops left
108
+ assert len(self.stitch_graph.nodes) == 0
109
+ self._last_loop = None
110
+ else: # Set to the newest loop formed at the end of any yarns.
111
+ self._last_loop = max(y.last_loop for y in self.yarns if isinstance(y.last_loop, Loop))
112
+
80
113
  def add_yarn(self, yarn: Yarn) -> None:
81
114
  """Add a yarn to the graph without adding its loops.
82
115
 
@@ -106,44 +139,31 @@ class Knit_Graph:
106
139
  self.stitch_graph.add_edge(parent_loop, child_loop, pull_direction=pull_direction)
107
140
  child_loop.add_parent_loop(parent_loop, stack_position)
108
141
 
109
- def get_wale_starting_with_loop(self, first_loop: Loop) -> Wale:
110
- """Get a wale (vertical column of stitches) starting from the specified loop.
142
+ def get_wales_ending_with_loop(self, last_loop: Loop) -> set[Wale]:
143
+ """Get all wales (vertical columns of stitches) that end at the specified loop.
111
144
 
112
145
  Args:
113
- first_loop (Loop): The loop at the start of the wale to be constructed.
146
+ last_loop (Loop): The last loop of the joined set of wales.
114
147
 
115
148
  Returns:
116
- Wale: A wale object representing the vertical column of stitches starting from the given loop.
149
+ set[Wale]: The set of wales that end at this loop.
117
150
  """
118
- wale = Wale(first_loop)
119
- cur_loop = first_loop
120
- while len(self.stitch_graph.successors(cur_loop)) == 1:
121
- cur_loop = [*self.stitch_graph.successors(cur_loop)][0]
122
- assert isinstance(wale.last_loop, Loop)
123
- wale.add_loop_to_end(cur_loop, self.get_pull_direction(wale.last_loop, cur_loop))
124
- return wale
125
-
126
- def get_wales_ending_with_loop(self, last_loop: Loop) -> list[Wale]:
127
- """Get all wales (vertical columns of stitches) that end at the specified loop.
151
+ if len(last_loop.parent_loops) == 0:
152
+ return {Wale(last_loop, self)}
153
+ ancestors = last_loop.ancestor_loops()
154
+ return {Wale(l, self) for l in ancestors}
128
155
 
129
- Args:
130
- last_loop (Loop): The last loop of the joined set of wales.
156
+ def get_terminal_wales(self) -> dict[Loop, list[Wale]]:
157
+ """
158
+ Get wale groups organized by their terminal loops.
131
159
 
132
160
  Returns:
133
- list[Wale]: The set of wales that end at this loop. Only returns multiple wales if this loop is a child of a decrease stitch.
161
+ dict[Loop, list[Wale]]: Dictionary mapping terminal loops to list of wales that terminate that wale.
134
162
  """
135
- wales = []
136
- if len(last_loop.parent_loops) == 0:
137
- return [Wale(last_loop)]
138
- for top_stitch_parent in last_loop.parent_loops:
139
- wale = Wale(last_loop)
140
- wale.add_loop_to_beginning(top_stitch_parent, cast(Pull_Direction, self.get_pull_direction(top_stitch_parent, last_loop)))
141
- cur_loop = top_stitch_parent
142
- while len(cur_loop.parent_loops) == 1: # stop at split for decrease or start of wale
143
- cur_loop = cur_loop.parent_loops[0]
144
- wale.add_loop_to_beginning(cur_loop, cast(Pull_Direction, self.get_pull_direction(cur_loop, cast(Loop, wale.first_loop))))
145
- wales.append(wale)
146
- return wales
163
+ wale_groups = {}
164
+ for loop in self.terminal_loops():
165
+ wale_groups[loop] = [wale for wale in self.get_wales_ending_with_loop(loop)]
166
+ return wale_groups
147
167
 
148
168
  def get_courses(self) -> list[Course]:
149
169
  """Get all courses (horizontal rows) in the knit graph in chronological order.
@@ -164,17 +184,13 @@ class Knit_Graph:
164
184
  courses.append(course)
165
185
  return courses
166
186
 
167
- def get_wale_groups(self) -> dict[Loop, Wale_Group]:
187
+ def get_wale_groups(self) -> set[Wale_Group]:
168
188
  """Get wale groups organized by their terminal loops.
169
189
 
170
190
  Returns:
171
- dict[Loop, Wale_Group]: Dictionary mapping terminal loops to the wale groups they terminate. Each wale group represents a collection of wales that end at the same terminal loop.
191
+ 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.
172
192
  """
173
- wale_groups = {}
174
- for loop in self:
175
- if self.is_terminal_loop(loop):
176
- wale_groups.update({loop: Wale_Group(wale, self) for wale in self.get_wales_ending_with_loop(loop)})
177
- return wale_groups
193
+ return set(Wale_Group(l, self) for l in self.terminal_loops())
178
194
 
179
195
  def __contains__(self, item: Loop | tuple[Loop, Loop]) -> bool:
180
196
  """Check if a loop is contained in the knit graph.
@@ -197,6 +213,12 @@ class Knit_Graph:
197
213
  """
198
214
  return cast(Iterator[Loop], iter(self.stitch_graph.nodes))
199
215
 
216
+ def __getitem__(self, item: int) -> Loop:
217
+ loop = next((l for l in self if l.loop_id == item), None)
218
+ if loop is None:
219
+ raise KeyError(f"Loop of id {item} not in knit graph")
220
+ return loop
221
+
200
222
  def sorted_loops(self) -> list[Loop]:
201
223
  """
202
224
  Returns:
@@ -270,3 +292,10 @@ class Knit_Graph:
270
292
  bool: True if the loop has no child loops and terminates a wale, False otherwise.
271
293
  """
272
294
  return not self.has_child_loop(loop)
295
+
296
+ def terminal_loops(self) -> Iterator[Loop]:
297
+ """
298
+ Returns:
299
+ Iterator[Loop]: An iterator over all terminal loops in the knit graph.
300
+ """
301
+ return iter(l for l in self if self.is_terminal_loop(l))
@@ -5,7 +5,7 @@ Loops are the fundamental building blocks of knitted structures and maintain rel
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
- from typing import TYPE_CHECKING, cast
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from knit_graphs.Yarn import Yarn
@@ -47,7 +47,12 @@ class Loop:
47
47
  Args:
48
48
  u (Loop): The first loop in the float pair.
49
49
  v (Loop): The second loop in the float pair.
50
+
51
+ Raises:
52
+ ValueError: If u and v are not on the same yarn.
50
53
  """
54
+ if u.yarn != v.yarn:
55
+ raise ValueError("Loops of a float must share a yarn.")
51
56
  if u not in self.back_floats:
52
57
  self.back_floats[u] = set()
53
58
  if v not in self.back_floats:
@@ -55,6 +60,18 @@ class Loop:
55
60
  self.back_floats[u].add(v)
56
61
  self.back_floats[v].add(u)
57
62
 
63
+ def remove_loop_from_front_floats(self) -> None:
64
+ """
65
+ Removes this loop from being in front of all marked floats. Mutates the yarns that own edges of those floats.
66
+ """
67
+ visited: set[Loop] = set()
68
+ for u, v_loops in self.front_floats.items():
69
+ visited.add(u)
70
+ for v in v_loops:
71
+ if v not in visited and v in u.yarn: # float shares a yarn
72
+ u.yarn.loop_graph.edges[u, v]["Back_Loops"].remove(self)
73
+ self.front_floats = {}
74
+
58
75
  def add_loop_behind_float(self, u: Loop, v: Loop) -> None:
59
76
  """Set this loop to be behind the float between loops u and v.
60
77
 
@@ -63,7 +80,12 @@ class Loop:
63
80
  Args:
64
81
  u (Loop): The first loop in the float pair.
65
82
  v (Loop): The second loop in the float pair.
83
+
84
+ Raises:
85
+ ValueError: If u and v are not on the same yarn.
66
86
  """
87
+ if u.yarn != v.yarn:
88
+ raise ValueError("Loops of a float must share a yarn.")
67
89
  if u not in self.front_floats:
68
90
  self.front_floats[u] = set()
69
91
  if v not in self.front_floats:
@@ -71,6 +93,18 @@ class Loop:
71
93
  self.front_floats[u].add(v)
72
94
  self.front_floats[v].add(u)
73
95
 
96
+ def remove_loop_from_back_floats(self) -> None:
97
+ """
98
+ Removes this loop from being behind of all marked floats. Mutates the yarns that own edges of those floats.
99
+ """
100
+ visited = set()
101
+ for u, v_loops in self.back_floats.items():
102
+ visited.add(u)
103
+ for v in v_loops:
104
+ if v not in visited and v in u.yarn: # float shares a yarn
105
+ u.yarn.loop_graph.edges[u, v]["Front_Loops"].remove(self)
106
+ self.back_floats = {}
107
+
74
108
  def is_in_front_of_float(self, u: Loop, v: Loop) -> bool:
75
109
  """Check if this loop is positioned in front of the float between loops u and v.
76
110
 
@@ -101,19 +135,25 @@ class Loop:
101
135
  Returns:
102
136
  Loop | None: The prior loop on the yarn, or None if this is the first loop on the yarn.
103
137
  """
104
- loop = self.yarn.prior_loop(self)
105
- if loop is None:
106
- return None
107
- else:
108
- return loop
138
+ return self.yarn.prior_loop(self)
109
139
 
110
- def next_loop_on_yarn(self) -> Loop:
140
+ def next_loop_on_yarn(self) -> Loop | None:
111
141
  """Get the loop that follows this loop on the same yarn.
112
142
 
113
143
  Returns:
114
- Loop: The next loop on the yarn, or None if this is the last loop on the yarn.
144
+ Loop | None: The next loop on the yarn, or None if this is the last loop on the yarn.
115
145
  """
116
- return cast(Loop, self.yarn.next_loop(self))
146
+ return self.yarn.next_loop(self)
147
+
148
+ def remove_parent_loops(self) -> list[Loop]:
149
+ """
150
+ Removes the list of parent loops from this loop.
151
+ Returns:
152
+ list[Loop]: The list of parent loops that were removed.
153
+ """
154
+ parents = self.parent_loops
155
+ self.parent_loops = []
156
+ return parents
117
157
 
118
158
  def has_parent_loops(self) -> bool:
119
159
  """Check if this loop has any parent loops connected through stitch edges.
@@ -135,6 +175,31 @@ class Loop:
135
175
  else:
136
176
  self.parent_loops.append(parent)
137
177
 
178
+ def remove_parent(self, parent: Loop) -> None:
179
+ """
180
+ Removes the given parent loop from the set of parents of this loop.
181
+ If the given loop is not a parent of this loop, nothing happens.
182
+ Args:
183
+ parent (Loop): The parent loop to remove.
184
+ """
185
+ if parent in self.parent_loops:
186
+ self.parent_loops.remove(parent)
187
+
188
+ def ancestor_loops(self) -> set[Loop]:
189
+ """
190
+ Returns:
191
+ set[Loop]: The set of loops that initiate all wales that lead to this loop. The empty set if this loop has no parents.
192
+ """
193
+ if not self.has_parent_loops():
194
+ return set()
195
+ ancestors = set()
196
+ for parent_loop in self.parent_loops:
197
+ if parent_loop.has_parent_loops():
198
+ ancestors.update(parent_loop.ancestor_loops())
199
+ else:
200
+ ancestors.add(parent_loop)
201
+ return ancestors
202
+
138
203
  @property
139
204
  def loop_id(self) -> int:
140
205
  """Get the unique identifier of this loop.
@@ -82,6 +82,8 @@ class Yarn:
82
82
  loop_graph (DiGraph): The directed graph loops connected by yarn-wise float edges.
83
83
  properties (Yarn_Properties): The physical and visual properties of this yarn.
84
84
  """
85
+ FRONT_LOOPS: str = "Front_Loops"
86
+ _BACK_LOOPS: str = "Back_Loops"
85
87
 
86
88
  def __init__(self, yarn_properties: None | Yarn_Properties = None, knit_graph: None | Knit_Graph = None):
87
89
  """Initialize a yarn with the specified properties and optional knit graph association.
@@ -141,7 +143,7 @@ class Yarn:
141
143
  self.add_loop_in_front_of_float(front_loop, v, u)
142
144
  else:
143
145
  return
144
- self.loop_graph.edges[u, v]["Front_Loops"].add(front_loop)
146
+ self.loop_graph.edges[u, v][self.FRONT_LOOPS].add(front_loop)
145
147
  front_loop.add_loop_in_front_of_float(u, v)
146
148
 
147
149
  def add_loop_behind_float(self, back_loop: Loop, u: Loop, v: Loop) -> None:
@@ -157,7 +159,7 @@ class Yarn:
157
159
  self.add_loop_behind_float(back_loop, v, u)
158
160
  else:
159
161
  return
160
- self.loop_graph.edges[u, v]["Back_Loops"].add(back_loop)
162
+ self.loop_graph.edges[u, v][self._BACK_LOOPS].add(back_loop)
161
163
  back_loop.add_loop_behind_float(u, v)
162
164
 
163
165
  def get_loops_in_front_of_float(self, u: Loop, v: Loop) -> set[Loop]:
@@ -176,7 +178,7 @@ class Yarn:
176
178
  else:
177
179
  return set()
178
180
  else:
179
- return cast(set[Loop], self.loop_graph.edges[u, v]['Front_Loops'])
181
+ return cast(set[Loop], self.loop_graph.edges[u, v][self.FRONT_LOOPS])
180
182
 
181
183
  def get_loops_behind_float(self, u: Loop, v: Loop) -> set[Loop]:
182
184
  """Get all loops positioned behind the float between two loops.
@@ -194,7 +196,7 @@ class Yarn:
194
196
  else:
195
197
  return set()
196
198
  else:
197
- return cast(set[Loop], self.loop_graph.edges[u, v]['Back_Loops'])
199
+ return cast(set[Loop], self.loop_graph.edges[u, v][self._BACK_LOOPS])
198
200
 
199
201
  @property
200
202
  def last_loop(self) -> Loop | None:
@@ -290,6 +292,41 @@ class Yarn:
290
292
  else:
291
293
  return self.last_loop.loop_id + 1
292
294
 
295
+ def remove_loop(self, loop: Loop) -> None:
296
+ """
297
+ Remove the given loop from the yarn.
298
+ Reconnects any neighboring loops to form a new float with the positioned in-front-of or behind the original floats positioned accordingly.
299
+ Resets the first_loop and last_loop properties if the removed loop was the tail of the yarn.
300
+ Args:
301
+ loop (Loop): The loop to remove from the yarn.
302
+
303
+ Raises:
304
+ KeyError: The given loop does not exist in the yarn.
305
+ """
306
+ if loop not in self:
307
+ raise KeyError(f'Loop {loop} does not exist on yarn {self}.')
308
+ prior_loop = self.prior_loop(loop)
309
+ next_loop = self.next_loop(loop)
310
+ if isinstance(prior_loop, Loop) and isinstance(next_loop, Loop): # Loop is between two floats to be merged.
311
+ front_of_float_loops = self.get_loops_in_front_of_float(prior_loop, loop)
312
+ front_of_float_loops.update(self.get_loops_in_front_of_float(loop, next_loop))
313
+ back_of_float_loops = self.get_loops_behind_float(prior_loop, loop)
314
+ back_of_float_loops.update(self.get_loops_behind_float(loop, next_loop))
315
+ self.loop_graph.remove_node(loop)
316
+ self.loop_graph.add_edge(prior_loop, next_loop, Front_Loops=front_of_float_loops, Back_Loops=back_of_float_loops)
317
+ for front_loop in front_of_float_loops:
318
+ front_loop.add_loop_in_front_of_float(prior_loop, next_loop)
319
+ for back_loop in back_of_float_loops:
320
+ back_loop.add_loop_behind_float(prior_loop, next_loop)
321
+ return
322
+ if next_loop is None: # This was the last loop, make the prior loop the last loop.
323
+ assert loop is self.last_loop
324
+ self._last_loop = prior_loop
325
+ if prior_loop is None: # This was the first loop, make the next loop the first loop.
326
+ assert loop is self.first_loop
327
+ self._first_loop = next_loop
328
+ self.loop_graph.remove_node(loop)
329
+
293
330
  def add_loop_to_end(self, loop: Loop) -> Loop:
294
331
  """Add an existing loop to the end of this yarn and associated knit graph.
295
332
 
@@ -19,6 +19,7 @@ class Loop_Braid_Graph:
19
19
  Attributes:
20
20
  loop_crossing_graph (DiGraph): A NetworkX directed graph storing loop crossing relationships with crossing direction attributes.
21
21
  """
22
+ _CROSSING = "crossing"
22
23
 
23
24
  def __init__(self) -> None:
24
25
  """Initialize an empty loop braid graph with no crossings."""
@@ -34,6 +35,15 @@ class Loop_Braid_Graph:
34
35
  """
35
36
  self.loop_crossing_graph.add_edge(left_loop, right_loop, crossing=crossing_direction)
36
37
 
38
+ def remove_loop(self, loop: Loop) -> None:
39
+ """
40
+ Removes any crossings that involve the given loop.
41
+ Args:
42
+ loop (Loop): The loop to remove.
43
+ """
44
+ if loop in self:
45
+ self.loop_crossing_graph.remove_node(loop)
46
+
37
47
  def __contains__(self, item: Loop | tuple[Loop, Loop]) -> bool:
38
48
  """Check if a loop or loop pair is contained in the braid graph.
39
49
 
@@ -92,4 +102,4 @@ class Loop_Braid_Graph:
92
102
  """
93
103
  if not self.loop_crossing_graph.has_edge(left_loop, right_loop):
94
104
  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'])
105
+ return cast(Crossing_Direction, self.loop_crossing_graph[left_loop][right_loop][self._CROSSING])
@@ -4,13 +4,16 @@ This module defines the Wale class which represents a vertical column of stitche
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- from typing import Iterator, cast
7
+ from typing import TYPE_CHECKING, Iterator, cast
8
8
 
9
9
  from networkx import DiGraph, dfs_preorder_nodes
10
10
 
11
11
  from knit_graphs.Loop import Loop
12
12
  from knit_graphs.Pull_Direction import Pull_Direction
13
13
 
14
+ if TYPE_CHECKING:
15
+ from knit_graphs.Knit_Graph import Knit_Graph
16
+
14
17
 
15
18
  class Wale:
16
19
  """A data structure representing stitch relationships between loops in a vertical column of a knitted structure.
@@ -23,49 +26,42 @@ class Wale:
23
26
  last_loop (Loop | None): The last (top) loop in the wale sequence.
24
27
  stitches (DiGraph): Stores the directed graph of stitch connections within this wale.
25
28
  """
29
+ _PULL_DIRECTION: str = "pull_direction"
26
30
 
27
- def __init__(self, first_loop: Loop | None = None) -> None:
31
+ def __init__(self, first_loop: Loop, knit_graph: Knit_Graph, end_loop: Loop | None = None) -> None:
28
32
  """Initialize a wale optionally starting with a specified loop.
29
33
 
30
34
  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.
35
+ first_loop (Loop): The initial loop to start the wale with.
36
+ knit_graph (Knit_Graph): The knit graph that owns this wale.
37
+ end_loop (Loop, optional):
38
+ The loop to terminate the wale with.
39
+ If no loop is provided or this loop is not found, the wale will terminate at the first loop with no child.
32
40
  """
33
41
  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.
42
+ self.stitches.add_node(first_loop)
43
+ self._knit_graph: Knit_Graph = knit_graph
44
+ self.first_loop: Loop = first_loop
45
+ self.last_loop: Loop = first_loop
46
+ self._build_wale_from_first_loop(end_loop)
47
+
48
+ def _build_wale_from_first_loop(self, end_loop: Loop | None) -> None:
49
+ while self._knit_graph.has_child_loop(self.last_loop):
50
+ child = self._knit_graph.get_child_loop(self.last_loop)
51
+ assert isinstance(child, Loop)
52
+ self.add_loop_to_end(child)
53
+ if end_loop is not None and child is end_loop:
54
+ return # found the end loop, so wrap up the wale
55
+
56
+ def add_loop_to_end(self, loop: Loop) -> None:
45
57
  """
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.
58
+ Add a loop to the end (top) of the wale with the specified pull direction.
57
59
 
58
60
  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.
61
+ loop (Loop): The loop to add to the end of the wale.
61
62
  """
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
63
+ self.stitches.add_edge(self.last_loop, loop, pull_direction=self._knit_graph.get_pull_direction(self.last_loop, loop))
64
+ self.last_loop = loop
69
65
 
70
66
  def get_stitch_pull_direction(self, u: Loop, v: Loop) -> Pull_Direction:
71
67
  """Get the pull direction of the stitch edge between two loops in this wale.
@@ -77,10 +73,11 @@ class Wale:
77
73
  Returns:
78
74
  Pull_Direction: The pull direction of the stitch between loops u and v.
79
75
  """
80
- return cast(Pull_Direction, self.stitches.edges[u, v]["pull_direction"])
76
+ return cast(Pull_Direction, self.stitches.edges[u, v][self._PULL_DIRECTION])
81
77
 
82
78
  def split_wale(self, split_loop: Loop) -> tuple[Wale, Wale | None]:
83
- """Split this wale at the specified loop into two separate wales.
79
+ """
80
+ Split this wale at the specified loop into two separate wales.
84
81
 
85
82
  The split loop becomes the last loop of the first wale and the first loop of the second wale.
86
83
 
@@ -93,19 +90,23 @@ class Wale:
93
90
  * The first wale (from start to split_loop). This will be the whole wale if the split_loop is not found.
94
91
  * The second wale (from split_loop to end). This will be None if the split_loop is not found.
95
92
  """
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:
93
+ if split_loop in self:
94
+ return (Wale(self.first_loop, self._knit_graph, end_loop=split_loop),
95
+ Wale(split_loop, self._knit_graph, end_loop=self.last_loop))
96
+ else:
107
97
  return self, None
108
- return first_wale, growing_wale
98
+
99
+ def __eq__(self, other: Wale) -> bool:
100
+ """
101
+ Args:
102
+ other (Wale): The wale to compare.
103
+
104
+ Returns:
105
+ bool: True if all the loops in both wales are present and in the same order. False, otherwise.
106
+ """
107
+ if len(self) != len(other):
108
+ return False
109
+ return not any(l != o for l, o in zip(self, other))
109
110
 
110
111
  def __len__(self) -> int:
111
112
  """Get the number of loops in this wale.
@@ -149,13 +150,28 @@ class Wale:
149
150
  return bool(self.stitches.has_node(item))
150
151
 
151
152
  def __hash__(self) -> int:
152
- """Get the hash value of this wale based on its first loop.
153
+ """
154
+ Get the hash value of this wale based on its first loop.
153
155
 
154
156
  Returns:
155
157
  int: Hash value based on the first loop in this wale.
156
158
  """
157
159
  return hash(self.first_loop)
158
160
 
161
+ def __str__(self) -> str:
162
+ """
163
+ Returns:
164
+ str: The string representation of this wale.
165
+ """
166
+ return f"Wale({self.first_loop}->{self.last_loop})"
167
+
168
+ def __repr__(self) -> str:
169
+ """
170
+ Returns:
171
+ str: The string representation of this wale.
172
+ """
173
+ return str(self)
174
+
159
175
  def overlaps(self, other: Wale) -> bool:
160
176
  """Check if this wale has any loops in common with another wale.
161
177
 
@@ -24,84 +24,66 @@ class Wale_Group:
24
24
  Attributes:
25
25
  wale_graph (DiGraph): A directed graph representing the relationships between wales in this group.
26
26
  stitch_graph (DiGraph): A directed graph of all individual stitch connections within this wale group.
27
- terminal_wale (Wale | None): The topmost wale in this group, typically where multiple wales converge.
28
27
  top_loops (dict[Loop, Wale]): Mapping from the last (top) loop of each wale to the wale itself.
29
28
  bottom_loops (dict[Loop, Wale]): Mapping from the first (bottom) loop of each wale to the wale itself.
30
29
  """
31
30
 
32
- def __init__(self, terminal_wale: Wale, knit_graph: Knit_Graph):
31
+ def __init__(self, terminal_loop: Loop, knit_graph: Knit_Graph):
33
32
  """Initialize a wale group starting from a terminal wale and building downward.
34
33
 
35
34
  Args:
36
- terminal_wale (Wale): The topmost wale in the group, used as the starting point for building the complete group structure.
35
+ terminal_loop (Loop): The terminal loop of this wale-group. All the wales in the group connect up to this loop.
37
36
  knit_graph (Knit_Graph): The parent knit graph that contains this wale group.
38
37
  """
39
38
  self.wale_graph: DiGraph = DiGraph()
40
39
  self.stitch_graph: DiGraph = DiGraph()
41
40
  self._knit_graph: Knit_Graph = knit_graph
42
- self.terminal_wale: Wale | None = terminal_wale
41
+ self._terminal_loop: Loop = terminal_loop
43
42
  self.top_loops: dict[Loop, Wale] = {}
44
43
  self.bottom_loops: dict[Loop, Wale] = {}
45
- self.build_group_from_top_wale(terminal_wale)
44
+ self._build_wale_group()
46
45
 
47
- def add_wale(self, wale: Wale) -> None:
48
- """Add a wale to the group and connect it to existing wales through shared loops.
49
-
50
- This method adds the wale to the group's graphs and establishes connections with other wales based on shared loops at their endpoints.
51
-
52
- Args:
53
- wale (Wale): The wale to add to this group. Empty wales are ignored and not added.
46
+ @property
47
+ def terminal_loop(self) -> Loop:
48
+ """
49
+ Returns:
50
+ Loop: The loop that terminates all wales in this group.
54
51
  """
55
- if len(wale) == 0:
56
- return # This wale is empty and therefore there is nothing to add to the wale group
52
+ return self._terminal_loop
53
+
54
+ def _build_wale_group(self) -> None:
55
+ full_wales = self._knit_graph.get_wales_ending_with_loop(self._terminal_loop)
56
+ # Build up the stitch graph.
57
+ for wale in full_wales:
58
+ u_loops: list[Loop] = cast(list[Loop], wale[:-1])
59
+ v_loops: list[Loop] = cast(list[Loop], wale[1:])
60
+ for u, v in zip(u_loops, v_loops):
61
+ self.stitch_graph.add_edge(u, v, pull_direction=self._knit_graph.get_pull_direction(u, v))
62
+ wales_to_split = full_wales
63
+ while len(wales_to_split) > 0:
64
+ wale_to_split = wales_to_split.pop()
65
+ split = False
66
+ upper_loops = cast(list[Loop], wale_to_split[1:])
67
+ for loop in upper_loops: # skip first loop in each wale as it may be already connected to a discovered decrease.
68
+ if len(loop.parent_loops) > 1: # Focal of a decrease.
69
+ clean_wale, remaining_wale = wale_to_split.split_wale(loop)
70
+ if not self.wale_graph.has_node(clean_wale):
71
+ self._add_wale(clean_wale)
72
+ if isinstance(remaining_wale, Wale):
73
+ wales_to_split.add(remaining_wale)
74
+ split = True
75
+ break
76
+ if not split:
77
+ self._add_wale(wale_to_split)
78
+ for bot_loop, lower_wale in self.bottom_loops.items():
79
+ if lower_wale.last_loop in self.bottom_loops:
80
+ self.wale_graph.add_edge(lower_wale, self.bottom_loops[lower_wale.last_loop])
81
+
82
+ def _add_wale(self, wale: Wale) -> None:
57
83
  self.wale_graph.add_node(wale)
58
- for u, v in wale.stitches.edges:
59
- self.stitch_graph.add_edge(u, v, pull_direction=wale.get_stitch_pull_direction(u, v))
60
- for top_loop, other_wale in self.top_loops.items():
61
- if top_loop == wale.first_loop:
62
- self.wale_graph.add_edge(other_wale, wale)
63
- for bot_loop, other_wale in self.bottom_loops.items():
64
- if bot_loop == wale.last_loop:
65
- self.wale_graph.add_edge(wale, other_wale)
66
- assert isinstance(wale.last_loop, Loop)
67
84
  self.top_loops[wale.last_loop] = wale
68
- assert isinstance(wale.first_loop, Loop)
69
85
  self.bottom_loops[wale.first_loop] = wale
70
86
 
71
- def add_parent_wales(self, wale: Wale) -> list[Wale]:
72
- """Find and add all parent wales that created the given wale through decrease operations.
73
-
74
- This method identifies wales that end at the loops that are parents of this wale's first loop, representing the wales that were decreased together to form the given wale.
75
-
76
- Args:
77
- wale (Wale): The wale to find and add parent wales for.
78
-
79
- Returns:
80
- list[Wale]: The list of parent wales that were found and added to the group.
81
- """
82
- added_wales = []
83
- for parent_loop in cast(Loop, wale.first_loop).parent_loops:
84
- parent_wales = self._knit_graph.get_wales_ending_with_loop(parent_loop)
85
- for parent_wale in parent_wales:
86
- self.add_wale(parent_wale)
87
- added_wales.extend(parent_wales)
88
- return added_wales
89
-
90
- def build_group_from_top_wale(self, top_wale: Wale) -> None:
91
- """Build the complete wale group by recursively finding all parent wales from the terminal wale.
92
-
93
- This method starts with the terminal wale and recursively adds all parent wales, building the complete tree structure of wales that contribute to the terminal wale through decrease operations.
94
-
95
- Args:
96
- top_wale (Wale): The terminal wale at the top of the group structure.
97
- """
98
- self.add_wale(top_wale)
99
- added_wales = self.add_parent_wales(top_wale)
100
- while len(added_wales) > 0:
101
- next_wale = added_wales.pop()
102
- more_wales = self.add_parent_wales(next_wale)
103
- added_wales.extend(more_wales)
104
-
105
87
  def get_loops_over_courses(self) -> list[list[Loop]]:
106
88
  """Get loops organized by their course (horizontal row) within this wale group.
107
89
 
@@ -110,11 +92,8 @@ class Wale_Group:
110
92
  Returns:
111
93
  list[list[Loop]]: A list where each inner list contains all loops that belong to the same course, ordered from top to bottom courses. Returns empty list if there is no terminal wale.
112
94
  """
113
- if self.terminal_wale is None:
114
- return []
115
- top_loop: Loop = cast(Loop, self.terminal_wale.last_loop)
116
95
  courses: list[list[Loop]] = []
117
- cur_course: list[Loop] = [top_loop]
96
+ cur_course: list[Loop] = [self._terminal_loop]
118
97
  while len(cur_course) > 0:
119
98
  courses.append(cur_course)
120
99
  next_course = []
@@ -136,3 +115,37 @@ class Wale_Group:
136
115
  path_len = sum(len(successor) for successor in dfs_preorder_nodes(self.wale_graph, wale))
137
116
  max_len = max(max_len, path_len)
138
117
  return max_len
118
+
119
+ def __hash__(self) -> int:
120
+ """
121
+ Returns:
122
+ int: Hash value of the terminal loop of this group.
123
+ """
124
+ return hash(self._terminal_loop)
125
+
126
+ def __contains__(self, item: Loop | int | Wale) -> bool:
127
+ """
128
+ Args:
129
+ item (Loop | int | Wale): The item to check for in the wale group.
130
+
131
+ Returns:
132
+ bool: True if the given loop, loop_id (int), or wale is in this group.
133
+ """
134
+ if isinstance(item, Loop) or isinstance(item, int):
135
+ return item in self.stitch_graph.nodes
136
+ else: # isinstance(item, Wale):
137
+ return item in self.wale_graph
138
+
139
+ def __str__(self) -> str:
140
+ """
141
+ Returns:
142
+ str: The string representation of this wale group.
143
+ """
144
+ return f"WG({self.terminal_loop})"
145
+
146
+ def __repr__(self) -> str:
147
+ """
148
+ Returns:
149
+ str: The string representation of this wale group.
150
+ """
151
+ return str(self)
File without changes
File without changes
File without changes
File without changes