Skip to content

Commit 9321e0a

Browse files
Add unstable/ directory with MyST text notebooks and converter script
- Convert 5 lessons + 5 exercise answers to .md MyST notebooks - Add unstable/myst.yml and unstable/README.md - Include convert_to_myst.py for reproducible conversion - Build verified: myst build --execute --html succeeds for all 11 pages
1 parent d82dbb0 commit 9321e0a

16 files changed

Lines changed: 3249 additions & 0 deletions

convert_to_myst.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
"""Convert Jupyter notebooks (.ipynb) to MyST text notebooks (.md).
3+
4+
Each code cell becomes a ````{code-cell}```` directive.
5+
Markdown cells are preserved as-is.
6+
Raw cells and latex macro cells are removed (handled by myst.yml).
7+
"""
8+
9+
import json
10+
import re
11+
import sys
12+
from pathlib import Path
13+
14+
15+
def _join_source(source):
16+
"""Join cell source lines, ensuring proper newlines between them.
17+
18+
Handles both formats:
19+
- ["line1\n", "line2\n"] (original ipynb format)
20+
- ["line1", "line2"] (json.dump re-saved without trailing newlines)
21+
"""
22+
if isinstance(source, str):
23+
return source
24+
parts = list(source)
25+
if not parts:
26+
return ""
27+
# Check if lines already have trailing newlines
28+
if parts and parts[0].endswith("\n"):
29+
return "".join(parts)
30+
# No trailing newlines — join with \n and add one at the end
31+
return "\n".join(parts) + "\n"
32+
33+
34+
def notebook_to_myst(nb_path: Path, output_path: Path, title_prefix: str = ""):
35+
"""Convert a single notebook to a MyST text notebook."""
36+
with open(nb_path) as f:
37+
nb = json.load(f)
38+
39+
lines: list[str] = []
40+
41+
# Frontmatter
42+
lines.append("---")
43+
lines.append("kernelspec:")
44+
lines.append(" name: python3")
45+
lines.append(" display_name: 'Python 3'")
46+
lines.append("---")
47+
lines.append("")
48+
49+
for cell in nb["cells"]:
50+
cell_type = cell["cell_type"]
51+
source = _join_source(cell["source"])
52+
53+
if not source.strip():
54+
continue
55+
56+
# Skip raw cells
57+
if cell_type == "raw":
58+
continue
59+
60+
# Skip latex macro definition cells
61+
lower_src = source.lower().strip()
62+
if "providecommand" in lower_src and ("myvec" in lower_src or "mymatrix" in lower_src):
63+
continue
64+
65+
if cell_type == "markdown":
66+
# Fix ipynb attachment syntax: ![img](attachment:img.png) -> ![img](img.png)
67+
fixed = re.sub(
68+
r'!\[(.*?)\]\(attachment:(.*?)\)',
69+
r'![\1](\2)',
70+
source,
71+
)
72+
lines.append(fixed.rstrip("\n"))
73+
lines.append("")
74+
75+
elif cell_type == "code":
76+
code = source.rstrip("\n")
77+
lines.append("````{code-cell}")
78+
lines.append(code)
79+
lines.append("````")
80+
lines.append("")
81+
82+
# Remove trailing blank lines but keep one
83+
while len(lines) > 1 and not lines[-1].strip():
84+
lines.pop()
85+
lines.append("")
86+
87+
output_path.write_text("\n".join(lines), encoding="utf-8")
88+
print(f" {nb_path} -> {output_path}")
89+
90+
91+
def main():
92+
base = Path(__file__).parent
93+
src_dir = base / "basic_lessons"
94+
dst_dir = base / "unstable"
95+
dst_dir.mkdir(exist_ok=True)
96+
97+
# Copy images
98+
for img in src_dir.glob("*.*"):
99+
if img.suffix.lower() in (".png", ".svg"):
100+
dst = dst_dir / img.name
101+
dst.write_bytes(img.read_bytes())
102+
print(f" Copied {img.name}")
103+
104+
# Convert tutorial notebooks
105+
tutorials = [
106+
"lesson1_tutorial.ipynb",
107+
"lesson2_tutorial.ipynb",
108+
"lesson3_tutorial.ipynb",
109+
"lesson4_tutorial.ipynb",
110+
"lesson5_tutorial.ipynb",
111+
]
112+
113+
exercise_answers = [
114+
"lesson1_exercise_answers.ipynb",
115+
"lesson2_exercise_answers.ipynb",
116+
"lesson3_exercise_answers.ipynb",
117+
"lesson4_exercise_answers.ipynb",
118+
"lesson5_exercise_answers.ipynb",
119+
]
120+
121+
print("Converting tutorials...")
122+
for nb in tutorials:
123+
src = src_dir / nb
124+
dst = dst_dir / nb.replace(".ipynb", ".md")
125+
notebook_to_myst(src, dst)
126+
127+
print("\nConverting exercise answers...")
128+
for nb in exercise_answers:
129+
src = src_dir / nb
130+
dst = dst_dir / nb.replace(".ipynb", ".md")
131+
notebook_to_myst(src, dst)
132+
133+
print("\nDone.")
134+
135+
136+
if __name__ == "__main__":
137+
main()

unstable/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# MyST build artifacts
2+
_build/

unstable/Lesson4.png

20.3 KB
Loading

unstable/Lesson4.svg

Lines changed: 290 additions & 0 deletions
Loading

unstable/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# [WIP] The Basics of Kinematic Modeling and Control of Serial-link Manipulators Using `numpy`
2+
3+
> **Warning:** These are text-based (MyST) notebooks under active development. The canonical `.ipynb` versions remain in [`basic_lessons/`](../basic_lessons/).
4+
5+
This directory contains the same five-lesson tutorial as [`basic_lessons/`](../basic_lessons/) but converted to
6+
[MyST text notebooks](https://mystmd.org/guide/notebooks-with-markdown). The content is identical; only the file format
7+
has changed from `.ipynb` to `.md` with `{code-cell}` directives.
8+
9+
## Contents
10+
11+
| Number | Title and Link | Content |
12+
|--------|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
13+
| 1 | [](./lesson1_tutorial.md) | Basic operations in Python and `numpy` |
14+
| 2 | [](./lesson2_tutorial.md) | Learn about elements and operations in $\mathbb{R}^n$, $SO(n)$, and $SE(n)$ with $n\in{\{2,3\}}$ related to positions, orientations, and poses, respectively. |
15+
| 3 | [](./lesson3_tutorial.md) | Learn about the composition of rigid body motion in series to obtain the forward kinematics model of a robotic manipulator. |
16+
| 4 | [](./lesson4_tutorial.md) | Learn about the first-order differential mapping $\dot{\myvec{x}}=\mymatrix{J}\dot{\myvec{q}}$ through the calculation of the Jacobian $\mymatrix{J}$. |
17+
| 5 | [](./lesson5_tutorial.md) | Employ the previous knowledge in all previous lessons to employ a Lyapunov-stable control law to move a manipulator in task space using configuration-space signals. |
18+
19+
### Exercise Answers
20+
21+
| Lesson | Link |
22+
|--------|------|
23+
| L1 | [](./lesson1_exercise_answers.md) |
24+
| L2 | [](./lesson2_exercise_answers.md) |
25+
| L3 | [](./lesson3_exercise_answers.md) |
26+
| L4 | [](./lesson4_exercise_answers.md) |
27+
| L5 | [](./lesson5_exercise_answers.md) |
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
kernelspec:
3+
name: python3
4+
display_name: 'Python 3'
5+
---
6+
7+
# L1 Exercise Answers
8+
9+
*License: CC-BY-NC-SA 4.0*
10+
11+
*Author: Murilo M. Marinho (murilo.marinho@manchester.ac.uk)*
12+
13+
### I found an issue
14+
Thank you! Please report it at https://github.com/MarinhoLab/OpenExecutableBooksRobotics/issues
15+
16+
### Latex Macros
17+
18+
# Valid imports
19+
20+
````{code-cell}
21+
from math import pi, sin, cos
22+
import numpy as np
23+
````
24+
25+
# Exercises
26+
27+
## Exercise 1
28+
29+
````{code-cell}
30+
phi = pi/4.0
31+
32+
e1 = sin(phi) + 4 * cos(phi / 5)
33+
34+
# Printing the result is NOT a mandatory part of the answer.
35+
print(f'e1 = {e1}')
36+
````
37+
38+
## Exercise 2
39+
40+
````{code-cell}
41+
A2 = np.array([[5, 2],
42+
[3, 5]])
43+
B2 = np.array([[5, 3],
44+
[3, 8]])
45+
46+
C2 = A2 + B2 + (A2 @ B2) - (B2 @ A2)
47+
48+
# Printing the result is NOT a mandatory part of the answer.
49+
print(f'C2 = {C2}')
50+
````

0 commit comments

Comments
 (0)