Lecture 19: Machine Knitting = Design with Constraints¶
In-Class Exercises¶
This notebook is the hands-on companion to today's slides. After we discuss the different types of constraints and the mental model for our class hierarchy, we'll work through these exercises together.
Recall from the slides:
KnitoutWriter (HW5) -- individual knitout instructions + validation
-> KnittingHelper (new) -- plain flat knitting operations
-> ShapingHelper (new) -- plus tubular knitting and shaping operations
Our goal: take the procedural helper functions you've been using all semester (from knitout_helpers.py in HW2, HW3, HW4) and turn them into methods on a class that inherits from KnitoutWriter.
Exercise 1: Spot the Hidden State¶
Here is the procedural cast_on function from HW2, followed by the loop that used it. Look at all the things being passed around or tracked externally:
# From HW2 knitout_helpers.py
width = 20
def cast_on(knitout_lines, carrier):
for s in range(width, 0, -1):
if (width - s) % 2 == 0:
knitout_lines.append(f"tuck - f{s} {carrier}")
for s in range(1, width + 1):
if (width - s) % 2 == 1:
knitout_lines.append(f"tuck + f{s} {carrier}")
# And in the main() code:
knitout_lines = []
write_headers(knitout_lines)
knitout_lines.append(f"inhook {carrier}")
cast_on(knitout_lines, carrier)
knit_waste(knitout_lines, carrier)
knitout_lines.append(f"releasehook {carrier}")
Question: What pieces of state are being threaded through these function calls? List them. Which ones are explicit parameters, and which are global/implicit? Discuss with your neighbors!
One way to think about this is...
Explicit (passed as arguments):
knitout_lines: the output list, passed to every functioncarrier: which yarn carrier to use
Implicit (global or managed by caller):
width: a constant defined at the top of the file, not passed to functions- direction: not tracked at all in
cast_on, but the caller has to know that after cast-on the direction is"-"(the next pass should go right-to-left). When multiple yarn carriers are involved, the caller needs to keep track the direction for individual carriers, e.g.,direction_A = "-",direction_B = "+", a variable the caller maintains separately. - which carriers are active: the caller manually calls
inhook/releasehookaroundcast_on. The function itself doesn't check whether the carrier is ready.
All of these become instance attributes in a class:
knitout_lines->self.instructions(inherited fromKnitoutWriter)carrier-> stays a parameter (which carrier to use is a per-function-call choice)width->self.width(an initial width to cast on for)direction->self.direction[carrier](a dictionary mapping from carrier to knitting direction)- active carriers ->
self.active_carriers(inherited fromKnitoutWriter)
Exercise 2: From Functions to Methods¶
Here is the skeleton of KnittingHelper. Let's fill in cast_on and knit_row by adapting the procedural versions from prior homework assignment knitout_helpers.py.
The key changes from procedural to OOP:
knitout_lines.append(f"knit ...")becomesself.knit(...)(inherited fromKnitoutWriter)widthbecomesself.width- Direction is tracked in
self.direction[carrier]and flipped after each pass inhookis called viaself.inhook(carrier)(inherited fromKnitoutWriter)
Using inherited methods will implicitly use the validations we have implemented already.
Procedural reference code (from HW2 knitout_helpers.py)¶
Here are the original procedural functions. You can use these as your guide. In this exercise, the goal is to translate the logic into methods on the class below.
def cast_on(knitout_lines, carrier):
"""Perform alternating tuck cast-on on front bed."""
for s in range(width, 0, -1):
if (width - s) % 2 == 0:
knitout_lines.append(f"tuck - f{s} {carrier}")
for s in range(1, width + 1):
if (width - s) % 2 == 1:
knitout_lines.append(f"tuck + f{s} {carrier}")
def knit_waste(knitout_lines, carrier):
"""Knit the waste rows (direction is hardcoded: -, +, -, +, ...)."""
for r in range(waste):
for s in range(width, 0, -1):
knitout_lines.append(f"knit - f{s} {carrier}")
for s in range(1, width + 1):
knitout_lines.append(f"knit + f{s} {carrier}")
Note: knit_waste hardcodes the direction pattern (-, +, -, +, ...) by always looping right-to-left then left-to-right. In our OOP version, we will make knit_row a separate method that knits an entire row, and it will respect self.direction[carrier] and flip it after each pass. Then knit_waste just calls knit_row in a loop.
from knitout_writer import KnitoutWriter
class KnittingHelper(KnitoutWriter):
def __init__(self, width):
super().__init__(carriers=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
self.width = width
self.direction = {c: "-" for c in self.carriers}
def _flip_direction(self, carrier):
"""Flip a carrier's direction after a pass."""
self.direction[carrier] = "+" if self.direction[carrier] == "-" else "-"
def cast_on(self, carrier):
"""
Alternating-tuck cast-on on the front bed.
After this, the carrier should be active with direction "-".
Hint: use self.tuck() and self.width.
"""
# TODO: your code here
pass
def knit_row(self, carrier):
"""
Knit one full-width pass on the front bed.
Respect self.direction[carrier], then flip it.
Hint: use self.knit() and self._flip_direction().
When you don't remember the function signature,
you can always use help(...) to figure out.
"""
# TODO: your code here
pass
def knit_waste(self, carrier, n_rows=4):
"""Knit waste rows (this one's done for you)."""
for _ in range(n_rows):
self.knit_row(carrier)
Test your solution¶
If the above implementation is correct, this should produce valid knitout:
# Run this cell after filling in the methods above:
kh = KnittingHelper(width=10)
kh.inhook(1)
kh.cast_on(1)
kh.knit_waste(1)
kh.releasehook(1)
for _ in range(5):
kh.knit_row(1)
kh.outhook(1)
for s in range(1, 11):
kh.drop(f"f{s}")
with open("Ex2.k", "w") as outfile:
output = kh.write()
outfile.write(output)
print(f"Generated {len(output.splitlines())} lines of knitout")
Solution¶
Click to reveal after you've tried it
def cast_on(self, carrier):
"""
Alternating-tuck cast-on on the front bed.
After this, the carrier should be active with direction "-".
Hint: use self.tuck() and self.width.
"""
for s in range(self.width, 0, -1):
if (self.width - s) % 2 == 0:
self.tuck("-", f"f{s}", carrier)
for s in range(1, self.width + 1):
if (self.width - s) % 2 == 1:
self.tuck("+", f"f{s}", carrier)
self.direction[carrier] = "-"
def knit_row(self, carrier):
"""
Knit one full-width pass on the front bed.
Respect self.direction[carrier], then flip it.
Hint: use self.knit() and self._flip_direction().
When you don't remember the function signature,
you can always use help(...) to figure out.
"""
if self.direction[carrier] == "-":
for s in range(self.width, 0, -1):
self.knit("-", f"f{s}", carrier)
else:
for s in range(1, self.width + 1):
self.knit("+", f"f{s}", carrier)
self._flip_direction(carrier)
Exercise 3: Shaping Helper (Specifically Short Rows)¶
Now let's go one level up. ShapingHelper inherits from KnittingHelper and adds state for shaped fabric that is created with shaping operations like increases, decreases, and short rows.
Short rows is a shaping technique that increases local height of the fabric: instead of knitting all the way across, you knit only partway, turn, and come back. By progressively knitting shorter and shorter rows, you create a wedge shape (this is how sock heels and shoulder slopes are made).
Here's the key idea:
Full row: knit ──────────────────────> (all 20 needles)
Short row 1: knit ─────────────────>tuck (stop at 2 to the right end, tuck at boundary)
Short row 2: tuck<───────────────── knit (stop at 2 to the left end, tuck at boundary)
Short row 3: knit ──────────────>tuck (4 to the right end)
Short row 4: tuck<────────────── knit (4 to the left end)
Why tuck at the turn point? When you reverse the knitting direction partway across, the yarn is not connected to the rest of the unknit row. This leaves a hole between the shorter rows and the rest of the rows. By tucking at the needle next to when we turn the knitting direction, you anchor the yarn and close the gap. This is a kind of constraint: every short-row turn needs a tuck to maintain fabric integrity.
Your task¶
Implement short_row_flat on ShapingHelper. We'll break it into three steps.
class ShapingHelper(KnittingHelper):
def __init__(self, width):
super().__init__(width)
self.min_n = 1
self.max_n = width
def short_row_flat(self, work_min, work_max, carrier):
"""
Knit one short-row pass on a flat piece.
All positions are absolute needle indices.
Parameters:
work_min: leftmost working needle (absolute index)
work_max: rightmost working needle (absolute index)
carrier: the yarn carrier to use
Procedure:
1. Validate [work_min, work_max] is within [self.min_n, self.max_n]
2. Knit only the working needles (respecting direction)
3. Tuck at the first held needle beyond the working range
(to close the gap and prevent a hole)
4. Flip the direction
Hints:
- When going "-" (right to left): knit work_max down to work_min,
then tuck at work_min - 1 (if work_min > self.min_n)
- When going "+" (left to right): knit work_min up to work_max,
then tuck at work_max + 1 (if work_max < self.max_n)
"""
# Step 1: Validate working range
# TODO: assert self.min_n <= work_min <= work_max <= self.max_n
# Step 2: Knit the working needles (check self.direction[carrier])
# TODO: knit from work_max to work_min (if "-") or work_min to work_max (if "+")
# Step 3: Tuck at the boundary to prevent a hole
# TODO: tuck at the first needle past the working range
# (Note: do nothing if that needle is outside of the [min_n,max_n] range!)
# Step 4: Flip direction
# TODO: call self._flip_direction(carrier)
pass
Discussion Questions¶
These questions might help with understanding the short rows logic. Discuss with your neighbors!
What happens if you remove the tuck? Imagine the yarn carrier is moving right-to-left and stops at
work_min. Without the tuck, what happens to the yarn betweenwork_minandwork_min - 1on the next pass?Edge case: What should happen when
work_min == self.min_n? Should the tuck still happen on the left side? Why or why not?Constraint families:
short_row_flatmanages constraints at multiple levels. Which parts handle shape-level concerns (what to knit), and which handle yarn-level concerns (preventing holes)?
Click to reveal the answers...
Without the tuck, the yarn floats loosely between
work_minandwork_min - 1. When you later "pick up" the held stitches by knitting a full row, there's a visible hole at each turn point. The tuck anchors the yarn to the first held needle, bridging the gap. On the machine, this prevents dropped stitches at the turn.When
work_min == self.min_n, there are no held needles on the left. So we're already knitting all the way to the edge! No tuck is needed because there's no gap to close. That's why the code checksif work_min > self.min_n(orwork_max < self.max_n) before tucking.Shape-level: validating the working range, deciding which needles to knit. Yarn-level: the tuck at the boundary, which prevents holes. Instruction-level (inherited):
self.knit(...)andself.tuck(...)validate the carrier and emit correct knitout syntax.
Test your short_row_flat¶
The cell below creates a wedge shape by progressively holding more needles on alternating sides. If your implementation is correct, it should run without errors.
# Test: create a wedge with short rows
sh = ShapingHelper(width=10)
sh.inhook(1)
sh.cast_on(1)
sh.knit_waste(1)
sh.releasehook(1)
# Knit some full rows first
for _ in range(4):
sh.knit_row(1)
# Now do short rows: progressively doing shorter back and forth
# All positions are absolute: work_min and work_max
for sr_end in range(1, 3):
sh.short_row_flat(work_min=sh.min_n + sr_end, work_max=sh.max_n - sr_end, carrier=1)
sh.short_row_flat(work_min=sh.min_n + sr_end, work_max=sh.max_n - sr_end, carrier=1)
# Knit some full rows to "pick up" the stitches held but not knitted
for _ in range(4):
sh.knit_row(1)
sh.outhook(1)
for s in range(1, 11):
sh.drop(f"f{s}")
with open("Ex3.k", "w") as outfile:
output = sh.write()
outfile.write(output)
print(f"Generated {len(output.splitlines())} lines of knitout")
Solution¶
Click to reveal after you've tried it
def short_row_flat(self, work_min, work_max, carrier):
# Step 1: Validate working range
assert self.min_n <= work_min <= work_max <= self.max_n, \
f"Working range [{work_min}, {work_max}] must be within [{self.min_n}, {self.max_n}]"
# Step 2: Knit the working needles
direction = self.direction[carrier]
if direction == "-":
for n in range(work_max, work_min - 1, -1):
self.knit("-", f"f{n}", carrier)
else:
for n in range(work_min, work_max + 1):
self.knit("+", f"f{n}", carrier)
# Step 3: Tuck at the boundary (only if there IS a held needle)
if direction == "-":
if work_min > self.min_n:
self.tuck("-", f"f{work_min - 1}", carrier)
else:
if work_max < self.max_n:
self.tuck("+", f"f{work_max + 1}", carrier)
# Step 4: Flip direction
self._flip_direction(carrier)
Key insights:
- Steps 1-2 are the shape-level logic: validate and decide which needles to work.
- Step 3 is the yarn-level constraint: tuck to prevent holes at the turn.
- The boundary check (
work_min > self.min_n) handles the edge case: if we're already at the edge, there's no held needle to tuck on, so we skip it. - Step 4 reverses direction so that the next call to
short_row_flat(orknit_row) will go the other way correctly. - All positions are absolute for easier management by the user who uses these methods. This design is consistent with the rest of the ShapingHelper API (decrease, increase, tubular short_rows all take absolute positions too).
What You're Getting for Your Project¶
The following starter code will be provided:
| File | Class | What it provides |
|---|---|---|
knitout_writer.pyc |
KnitoutWriter |
Compiled bytecode of KnitoutWriter class |
knitting_helper.py |
KnittingHelper |
Flat knitting + birdseye + basic colorwork |
shaping_helper.py |
ShapingHelper |
Tubular cast-on, short rows, inc/dec, tubular bind-off |
To use the precompiled .pyc files, place them directly under the same directory of the other code files that will import the file, and make sure that your Python version is 3.14.xx. You can check with the following command:
conda activate comp116
python -V
If your Python version is not 3.14, please follow the setup instructions from Ed to create an environment with this Python version. Please reach out if you run into any issues.
When to Use Which File¶
If your project is flat knitting or colorwork: use KnittingHelper directly.
If your project involves shaping and tubular knitting: use ShapingHelper.
If you've already started your own approach: keep going! These new helper files are completely optional. In your final writeup, a short comparison of your approach vs. the helpers would be great.
All files are in the updated project_starter_code.zip on Ed. Feel free to read, modify, and extend them.
How to Use Each File¶
Please refer to the API reference (API stands for application programming interface, in this case it just refers to the public-facing methods for the helper classes) to understand how each helper and the associated methods work and how to use them in your code.
Bringing this back to the project...¶
Before we wrap up, let's try to apply the lens of constraints to your projects.
Which of the constraint types from today's slides is your project most at risk of violating?
If it's a hard constraint, make sure your code catches it. If it's soft, then it becomes a design decision of how to handle it. Either way, hopefully you can apply what you learned from today's lecture to your projects and see you at the project work session!