-
-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathnumpy-pyodide-lab.html
More file actions
352 lines (325 loc) · 18.7 KB
/
numpy-pyodide-lab.html
File metadata and controls
352 lines (325 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NumPy Vectors & Matrices — Pyodide Lab</title>
<!-- Tailwind (optional, used for the utility classes in your component) -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Small safety styles so it looks okay even if some Tailwind classes are missing */
body { background: #f8fafc; }
.container { max-width: 1100px; margin: 0 auto; }
</style>
<!-- React 18 + ReactDOM 18 (UMD builds) -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel Standalone to compile JSX in-browser -->
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
</head>
<body>
<div id="app" class="container"></div>
<!-- Your app code: paste your component here. Type text/babel so JSX works. -->
<script type="text/babel">
const { useEffect, useMemo, useState } = React;
// NumPy Basics with Pyodide — Interactive Canvas (Patched)
function NumpyVectorsMatricesPyodide_Patched() {
const [pyodide, setPyodide] = useState(null);
const [status, setStatus] = useState("Loading Pyodide…");
const [ready, setReady] = useState(false);
const [running, setRunning] = useState(false);
// --- Load Pyodide + NumPy ---
useEffect(() => {
let cancelled = false;
async function boot() {
try {
setStatus("Loading Python runtime…");
// Inject the Pyodide script if not present
await new Promise((resolve, reject) => {
if (window.loadPyodide) return resolve();
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js";
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
setStatus("Starting Pyodide…");
const py = await window.loadPyodide({ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.1/full/" });
setStatus("Installing NumPy…");
await py.loadPackage("numpy");
// Warmup to verify stdout capture
await py.runPythonAsync("import numpy as np\nprint('pyodide warmup ok')\n'ready'");
if (!cancelled) {
setPyodide(py);
setReady(true);
setStatus("Ready");
}
} catch (err) {
console.error(err);
setStatus("Failed to load Pyodide. Please reload the page.");
}
}
boot();
return () => { cancelled = true; };
}, []);
// --- Lesson text (quick reference) ---
const lesson = (
<div className="prose prose-slate max-w-none">
<h1>NumPy: Basic Vector & Matrix Operations</h1>
<p>
A <strong>vector</strong> is a 1D array (shape like <code>(n,)</code>), and a <strong>matrix</strong> is a 2D array (shape like <code>(m, n)</code>).
In NumPy, both are represented by <code>numpy.ndarray</code>.
</p>
<ul>
<li>
Create arrays: <code>a = np.array([1, 2, 3])</code>,
<code>A = np.array([[1,2],[3,4]])</code>
</li>
<li>
Shapes: <code>a.shape</code>, <code>A.shape</code>
</li>
<li>
Elementwise ops: <code>a + b</code>, <code>2*a - b</code>, <code>a * b</code>
</li>
<li>
Dot product: <code>np.dot(a, b)</code>
</li>
<li>
Matrix multiply: <code>A @ B</code> or <code>np.matmul(A, B)</code>
</li>
<li>
Transpose: <code>A.T</code>
</li>
<li>
Indexing/slicing: <code>A[0, 1]</code>, <code>A[:, 0]</code>, <code>A[1:3, :]</code>
</li>
<li>
Broadcasting: <code>A + v</code> (if shapes are compatible)
</li>
<li>
Norm: <code>np.linalg.norm(a)</code>
</li>
</ul>
</div>
);
// --- Exercises ---
const exercises = useMemo(() => [
{
id: "ex1",
title: "Vectors: create & elementwise operations",
prompt:
"Create 1D vectors a and b of length 3. Compute c = a + b and d = 2*a - b. Print c and d, and confirm shapes are (3,).",
starter: `import numpy as np\n\n# Create vectors a and b (length 3)\na = np.array([1, 2, 3]) # you may change values\nb = np.array([4, 5, 6])\n\n# Compute elementwise results\nc = a + b\nd = 2*a - b\n\nprint('a shape:', a.shape)\nprint('b shape:', b.shape)\nprint('c:', c)\nprint('d:', d)\n`,
validator: `def validate(ns):\n import numpy as np\n a, b = ns.get('a'), ns.get('b')\n c, d = ns.get('c'), ns.get('d')\n assert isinstance(a, np.ndarray) and isinstance(b, np.ndarray), 'a and b must be numpy arrays'\n assert a.shape == (3,) and b.shape == (3,), 'a and b must have shape (3,)'\n assert isinstance(c, np.ndarray) and isinstance(d, np.ndarray), 'compute c and d as arrays'\n assert np.allclose(c, a + b), 'c must equal a + b'\n assert np.allclose(d, 2*a - b), 'd must equal 2*a - b'\n print('✅ Passed: shapes and elementwise ops are correct!')\n return True\n`,
},
{
id: "ex2",
title: "Dot product & Euclidean norm",
prompt:
"Given vectors a and b (same length), compute dot = a·b and the Euclidean norm of a. Print both.",
starter: `import numpy as np\n\n# Define vectors a and b\na = np.array([1., 2., 3.])\nb = np.array([0.5, -1., 4.])\n\n# Compute the dot product and the norm of a\ndot = np.dot(a, b)\nna = np.linalg.norm(a)\n\nprint('dot(a,b) =', dot)\nprint('||a|| =', na)\n`,
validator: `def validate(ns):\n import numpy as np\n a, b = ns.get('a'), ns.get('b')\n dot, na = ns.get('dot'), ns.get('na')\n assert a is not None and b is not None, 'define a and b'\n assert np.isscalar(dot), 'dot should be a scalar'\n assert np.isscalar(na), 'na should be a scalar'\n assert np.isclose(dot, float(np.dot(a, b))), 'dot product incorrect'\n assert np.isclose(na, float(np.linalg.norm(a))), 'norm incorrect'\n print('✅ Passed: dot product and norm look good!')\n return True\n`,
},
{
id: "ex3",
title: "Matrix multiply & transpose",
prompt:
"Create A (2×3) and B (3×2). Compute C = A @ B, its shape, and C[0,1]. Also compute A.T (transpose).",
starter: `import numpy as np\n\n# Define matrices with compatible shapes\nA = np.array([[1, 2, 3],\n [4, 5, 6]]) # 2x3\nB = np.array([[7, 8],\n [9, 10],\n [11,12]]) # 3x2\n\nC = A @ B\nprint('C shape:', C.shape)\nprint('C[0,1]:', C[0,1])\nprint('A.T:\\n', A.T)\n`,
validator: `def validate(ns):\n import numpy as np\n A, B, C = ns.get('A'), ns.get('B'), ns.get('C')\n assert A.shape == (2,3) and B.shape == (3,2), 'A must be 2x3 and B must be 3x2'\n assert C.shape == (2,2), 'C should be 2x2'\n assert np.allclose(C, A @ B), 'C must equal A @ B'\n assert np.allclose(ns.get('A').T, ns.get('A').transpose()), 'transpose should match'\n print('✅ Passed: matmul, shape, index, and transpose are correct!')\n return True\n`,
},
{
id: "ex4",
title: "Broadcasting a row vector",
prompt:
"Create M (3×3) and row vector v (shape (3,)). Compute S = M + v (adds v to each row). Print S.",
starter: `import numpy as np\n\nM = np.array([[1,2,3],\n [4,5,6],\n [7,8,9]])\nv = np.array([10,20,30])\n\nS = M + v\nprint(S)\n`,
validator: `def validate(ns):\n import numpy as np\n M, v, S = ns.get('M'), ns.get('v'), ns.get('S')\n assert M.shape == (3,3), 'M must be 3x3'\n assert v.shape == (3,), 'v must be shape (3,)'\n assert np.allclose(S, M + v), 'S must equal M + v with broadcasting'\n print('✅ Passed: broadcasting works!')\n return True\n`,
},
{
id: "ex5",
title: "Indexing and slicing",
prompt:
"From a 4×4 matrix X, extract the 2×2 top-left block (call it TL) and the last column (call it last_col). Print both.",
starter: `import numpy as np\n\nX = np.arange(1, 17).reshape(4,4)\n# X = [[ 1, 2, 3, 4],\n# [ 5, 6, 7, 8],\n# [ 9, 10, 11, 12],\n# [13, 14, 15, 16]]\n\nTL = X[0:2, 0:2]\nlast_col = X[:, 3]\n\nprint('TL:\\n', TL)\nprint('last_col:', last_col)\n`,
validator: `def validate(ns):\n import numpy as np\n X = ns.get('X')\n TL = ns.get('TL')\n last_col = ns.get('last_col')\n assert X.shape == (4,4), 'X must be 4x4'\n assert TL.shape == (2,2), 'TL must be 2x2'\n assert last_col.shape == (4,), 'last_col must be a 1D length-4 vector'\n assert np.allclose(TL, X[:2, :2]), 'TL should be top-left 2x2'\n assert np.allclose(last_col, X[:, -1]), 'last_col should be last column'\n print('✅ Passed: indexing and slicing look correct!')\n return True\n`,
},
], []);
// --- Helpers to run Python ---
async function runUserCode(code) {
if (!pyodide) return { user: "", checks: "Pyodide not ready." };
const py = pyodide;
const pySrc = `\nimport sys, io, contextlib, numpy as np\n_user_out = ''\nns = {'np': np}\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n try:\n exec(${JSON.stringify(code)}, ns)\n except Exception as e:\n print('❌ Error while running your code:', e)\n _user_out = buf.getvalue()\n_user_out\n`;
try {
const out = await py.runPythonAsync(pySrc);
const outStr = out && out.toString ? out.toString() : String(out ?? '');
return { user: outStr, checks: "" };
} catch (e) {
return { user: "", checks: String(e) };
}
}
async function runAndCheck(code, validatorSrc) {
if (!pyodide) return { user: "", checks: "Pyodide not ready." };
const py = pyodide;
const pySrc = `\nimport sys, io, contextlib, numpy as np\n_user_out = ''\n_check_out = ''\n_ok = False\nns = {'np': np}\n# Run user code and capture prints\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n try:\n exec(${JSON.stringify(code)}, ns)\n except Exception as e:\n print('❌ Error while running your code:', e)\n _user_out = buf.getvalue()\n\n# Define validator\n${validatorSrc}\n\n# Run validator and capture prints\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n try:\n _ok = bool(validate(ns))\n except AssertionError as e:\n print('❌', e)\n _ok = False\n except Exception as e:\n print('❌ Validator error:', e)\n _ok = False\n _check_out = buf.getvalue()\n\n{'ok': _ok, 'user': _user_out, 'checks': _check_out}\n`;
try {
const res = await py.runPythonAsync(pySrc);
let jsRes;
try {
jsRes = res.toJs ? res.toJs({ dict_converter: Object.fromEntries }) : res;
} catch {
jsRes = res;
}
if (jsRes instanceof Map) jsRes = Object.fromEntries(jsRes);
if (typeof jsRes.user !== 'string') jsRes.user = String(jsRes.user ?? '');
if (typeof jsRes.checks !== 'string') jsRes.checks = String(jsRes.checks ?? '');
return jsRes;
} catch (e) {
return { ok: false, user: "", checks: String(e) };
}
}
// --- Exercise Card Component ---
function ExerciseCard({ idx, ex }) {
const [code, setCode] = useState(ex.starter);
const [output, setOutput] = useState("");
const [checks, setChecks] = useState("");
const [ok, setOk] = useState(null);
const onRun = async () => {
setChecks("");
const res = await runUserCode(code);
setOutput(res.user ?? String(res));
};
const onCheck = async () => {
const res = await runAndCheck(code, ex.validator);
setOutput(res.user ?? "");
setChecks(res.checks ?? "");
setOk(Boolean(res.ok));
};
const onReset = () => {
setCode(ex.starter);
setOutput("");
setChecks("");
setOk(null);
};
return (
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold">{idx}. {ex.title}</h3>
<p className="mt-1 text-sm text-slate-600">{ex.prompt}</p>
</div>
<div className="flex items-center gap-2">
{ok === true && <span title="Passed" className="text-2xl">✅</span>}
{ok === false && <span title="Needs work" className="text-2xl">❌</span>}
</div>
</div>
<div className="mt-3 grid gap-3">
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
className="min-h-[160px] w-full resize-y rounded-xl border p-3 font-mono text-sm"
spellCheck={false}
/>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={onRun}
disabled={!ready}
className="rounded-xl bg-black px-4 py-2 text-white disabled:opacity-50"
>
▶ Run
</button>
<button
type="button"
onClick={onCheck}
disabled={!ready}
className="rounded-xl border px-4 py-2 disabled:opacity-50"
>
✓ Check
</button>
<button
type="button"
onClick={onReset}
className="rounded-xl border px-4 py-2"
>
↺ Reset
</button>
</div>
<div className="grid gap-2">
<label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Output</label>
<pre className="min-h-[64px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{output || "(no output)"}</pre>
</div>
<div className="grid gap-2">
<label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Checks</label>
<pre className="min-h-[40px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{checks || "(run ✓ Check to see feedback)"}</pre>
</div>
</div>
</div>
);
}
// --- Playground ---
const [playCode, setPlayCode] = useState(`import numpy as np\n\n# Try anything here. Some quick demos:\na = np.array([1, 2, 3])\nb = np.array([4, 5, 6])\nprint('a + b =', a + b)\n\nA = np.array([[1,2],[3,4]])\nB = np.array([[5,6],[7,8]])\nprint('A @ B =\\n', A @ B)\n`);
const [playOut, setPlayOut] = useState("");
const onRunPlay = async () => {
const res = await runUserCode(playCode);
setPlayOut(res.user ?? String(res));
};
return (
<div className="mx-auto max-w-5xl p-6">
<div className="mb-6 rounded-2xl border bg-white p-5 shadow-sm">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">NumPy Basics — Interactive Lab</h2>
<span className="text-sm text-slate-600">{status}</span>
</div>
<p className="mt-2 text-slate-700">
Learn core vector & matrix operations in NumPy, then practice with short exercises. Use the
Playground at the end to experiment. If something fails, read the error in the Output pane and
try again — that’s normal while learning!
</p>
</div>
<div className="mb-6 rounded-2xl border bg-white p-5 shadow-sm">{lesson}</div>
<div className="grid gap-5">
{exercises.map((ex, i) => (
<ExerciseCard key={ex.id} idx={i + 1} ex={ex} />
))}
</div>
<div className="mt-8 rounded-2xl border bg-white p-5 shadow-sm">
<h3 className="text-lg font-semibold">Playground</h3>
<p className="mt-1 text-sm text-slate-600">
A free space to run any NumPy code. NumPy is preloaded as <code>np</code>.
</p>
<div className="mt-3 grid gap-3">
<textarea
value={playCode}
onChange={(e) => setPlayCode(e.target.value)}
className="min-h-[160px] w-full resize-y rounded-xl border p-3 font-mono text-sm"
spellCheck={false}
/>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onRunPlay}
disabled={!ready}
className="rounded-2xl bg-black px-4 py-2 text-white disabled:opacity-50"
>
▶ Run
</button>
</div>
<div className="grid gap-2">
<label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Output</label>
<pre className="min-h-[64px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{playOut || "(no output)"}</pre>
</div>
</div>
</div>
<footer className="mt-10 text-center text-xs text-slate-500">
Tip: Use <code>a.shape</code> and <code>A.shape</code> often to sanity-check your math.
</footer>
</div>
);
}
// Mount the app
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(React.createElement(NumpyVectorsMatricesPyodide_Patched));
</script>
</body>
</html>