diff --git a/notebooks/COURSE_BLUEPRINT.ipynb b/notebooks/COURSE_BLUEPRINT.ipynb index 1f0a561..7cc4804 100644 --- a/notebooks/COURSE_BLUEPRINT.ipynb +++ b/notebooks/COURSE_BLUEPRINT.ipynb @@ -7,22 +7,10 @@ "role": "meta", "difficulty": 1, "kind": "orientation", - "title": "Why This Order Exists" + "title": "What This Course Separates" } }, - "source": "## META | difficulty 1 | Why This Order Exists\n\nThe course separates three ideas that are often blurred together:\n\n- the algebraic purpose of the NTT\n- the in-place butterfly dataflow\n- Kyber-specific implementation conventions\n" - }, - { - "cell_type": "markdown", - "metadata": { - "pedagogy": { - "role": "mandatory", - "difficulty": 2, - "kind": "route", - "title": "Learning Staircase" - } - }, - "source": "## MANDATORY | difficulty 2 | Learning Staircase\n\nThe supported staircase is:\n\n1. ordinary polynomial multiplication and convolution\n2. negacyclic multiplication\n3. a tiny toy NTT\n4. butterfly mechanics in isolation\n5. forward and inverse flow side by side\n6. Kyber-specific parameters and indexing\n7. real implementation patterns\n" + "source": "## META | difficulty 1 | What This Course Separates\n\nThis course keeps three stories separate on purpose:\n\n- the algebraic purpose of the transform\n- the local in-place butterfly dataflow\n- the Kyber-specific implementation conventions\n\nThe point is to stop those three from collapsing into one blurry \u201cFFT-like thing\u201d.\n" }, { "cell_type": "markdown", @@ -31,22 +19,34 @@ "role": "mandatory", "difficulty": 2, "kind": "structure", - "title": "Bundle Rhythm" + "title": "The Learning Staircase" } }, - "source": "## MANDATORY | difficulty 2 | Bundle Rhythm\n\nTechnical bundles follow a consistent rhythm:\n\n- `lecture.ipynb` explains the idea carefully\n- `lab.ipynb` asks for predictions before execution\n- `problems.ipynb` checks retrieval and reflection\n- `studio.ipynb` compares implementation choices and debugging cues\n" + "source": "## MANDATORY | difficulty 2 | The Learning Staircase\n\nThe supported staircase is:\n\n1. schoolbook multiplication and diagonals\n2. cyclic and negacyclic wraparound\n3. direct negative-wrapped NTT and iNTT\n4. fast forward CT butterflies\n5. fast inverse GS butterflies\n6. bit-reversal and ordering\n7. Kyber parameter reality and base multiplication\n8. debugging wrong sign / wrong zeta / wrong order / wrong scale failures\n" }, { "cell_type": "markdown", "metadata": { "pedagogy": { - "role": "meta", - "difficulty": 1, - "kind": "constraints", - "title": "Route Constraints" + "role": "mandatory", + "difficulty": 2, + "kind": "bundles", + "title": "Bundles" } }, - "source": "## META | difficulty 1 | Route Constraints\n\nRoute notebooks stay pure route notebooks.\n\n- no facultative detours here\n- no hidden competing learner route\n- every notebook ends with a visible handoff\n" + "source": "## MANDATORY | difficulty 2 | Bundles\n\nEach serious module uses the same rhythm:\n\n- `lecture.ipynb` = slow explanation plus visual demos\n- `lab.ipynb` = prediction before execution\n- `problems.ipynb` = retrieval and reflection\n- `studio.ipynb` = comparison, debugging, and implementation reading\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "expectation", + "title": "What \u201cBlunt And Graphical\u201d Means Here" + } + }, + "source": "## MANDATORY | difficulty 2 | What \u201cBlunt And Graphical\u201d Means Here\n\nThe notebooks should not ask the learner to imagine too much in their head.\n\nExpect:\n\n- schoolbook product grids\n- wraparound arrows\n- explicit stage arrays\n- stage sliders\n- bit-reversal wire maps\n- side-by-side wrong vs right traces\n" }, { "cell_type": "markdown", @@ -72,14 +72,35 @@ }, "ntt_learning": { "title": "Course Blueprint", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/COURSE_COMPLETE.ipynb b/notebooks/COURSE_COMPLETE.ipynb new file mode 100644 index 0000000..454e4d2 --- /dev/null +++ b/notebooks/COURSE_COMPLETE.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Route Complete" + } + }, + "source": "## META | difficulty 1 | Route Complete\n\nYou reached the end of the supported route.\n\nIf the course did its job, you should now be able to separate:\n\n- raw polynomial multiplication\n- negacyclic structure\n- direct NTT / iNTT definitions\n- fast CT / GS butterfly flow\n- order changes and scaling\n- Kyber\u2019s specific modulus and base-multiplication story\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Exit Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Exit Reflection\n\nFinal written prompts:\n\n1. Explain the difference between \u201cthe transform as mathematics\u201d and \u201cthe butterfly network as an implementation strategy\u201d.\n2. Explain why Kyber v3 needs more than the naive \u201cjust use a 2n-th root\u201d mental model.\n3. Name the first four checks you would run if an iNTT output looked wrong.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: you are at the end of the supported route. Revisit the studios if you want more repetition.\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Course Complete", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/START_HERE.ipynb b/notebooks/START_HERE.ipynb index 03d0eac..0ae1b66 100644 --- a/notebooks/START_HERE.ipynb +++ b/notebooks/START_HERE.ipynb @@ -10,7 +10,7 @@ "title": "Welcome" } }, - "source": "## META | difficulty 1 | Welcome\n\nThis course is local-first and notebook-first. Every visible cell is labeled so the learner always knows\nwhether a cell is route guidance, required walkthrough material, or an optional detour.\n\nContract:\n\n- `META` = route, pacing, and handoff guidance\n- `MANDATORY` = the official walkthrough\n- `FACULTATIVE` = optional extension\n" + "source": "## META | difficulty 1 | Welcome\n\nThis course is for people who need to see the algorithm move.\n\nThe goal is not to hide behind abstract formulas. The goal is to make the NTT and iNTT feel physically inspectable:\n\n- every stage should look like values moving through wires\n- every wraparound should be visible\n- every ordering change should be concrete\n- every Kyber-specific choice should be motivated by what the arithmetic allows\n" }, { "cell_type": "markdown", @@ -22,7 +22,19 @@ "title": "Official Route" } }, - "source": "## MANDATORY | difficulty 1 | Official Route\n\nFollow exactly one supported path:\n\n1. `START_HERE.ipynb`\n2. `COURSE_BLUEPRINT.ipynb`\n3. `notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb`\n4. `notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb`\n5. `notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb`\n6. `notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb`\n\nThe course starts with concrete arrays and small examples before any Kyber-specific implementation details.\n" + "source": "## MANDATORY | difficulty 1 | Official Route\n\nFollow exactly one supported route:\n\n1. `START_HERE.ipynb`\n2. `COURSE_BLUEPRINT.ipynb`\n3. each bundle in `Lecture -> Lab -> Problems -> Studio` order\n4. `COURSE_COMPLETE.ipynb`\n\nSupported bundles:\n\n- `foundations/01_convolution_to_toy_ntt`\n- `foundations/02_negative_wrapped_ntt`\n- `butterfly_mechanics/03_fast_forward_ct`\n- `butterfly_mechanics/04_fast_inverse_gs`\n- `kyber_mapping/05_kyber_ntt_and_base_multiplication`\n- `professional/06_debugging_ntt_failures`\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 1, + "kind": "contract", + "title": "Visible Cell Contract" + } + }, + "source": "## MANDATORY | difficulty 1 | Visible Cell Contract\n\nCell labels are not decoration. They tell you how to use the notebook:\n\n- `META` = route, pacing, and handoff\n- `MANDATORY` = the official walkthrough\n- `FACULTATIVE` = optional deepening only\n- difficulty `1-3` is reserved for mandatory work\n- difficulty `4-10` is reserved for facultative work\n" }, { "cell_type": "markdown", @@ -34,7 +46,7 @@ "title": "Local Operations" } }, - "source": "## META | difficulty 1 | Local Operations\n\nRepo-local commands:\n\n- `scripts/bootstrap.sh` creates `.venv` and installs dependencies\n- `scripts/validate.sh` runs structural and execution checks\n- `scripts/start.sh` launches JupyterLab when it is installed\n" + "source": "## META | difficulty 1 | Local Operations\n\nRepo-local commands:\n\n- `scripts/bootstrap.sh`\n- `scripts/start.sh`\n- `scripts/status.sh`\n- `scripts/validate.sh`\n" }, { "cell_type": "markdown", @@ -60,14 +72,35 @@ }, "ntt_learning": { "title": "Start Here", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb b/notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb new file mode 100644 index 0000000..b7699d0 --- /dev/null +++ b/notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Lab Goals" + } + }, + "source": "## META | difficulty 1 | Lab Goals\n\nYou should predict stage pairings and zetas before running the stage explorer.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 1" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 1\n\nFor the `n = 4` example, predict:\n\n- which original coefficients pair in stage 1\n- which adjacent positions pair in stage 2\n- why the output is not yet in normal order\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check For n=4" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check For n=4\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace\nfrom ntt_learning.visuals import interactive_trace\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\ndisplay(interactive_trace(trace, title=\"Check your n=4 prediction\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 2" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 2\n\nFor the `n = 8` example, name the stage count before you run the next cell.\nThen name which stage feels most confusing and why.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check For n=8" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check For n=8\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi\nfrom ntt_learning.visuals import interactive_trace\n\ntrace = fast_ntt_psi_ct_trace([0, 1, 2, 3, 4, 5, 6, 7], 97, find_psi(8, 97))\ndisplay(interactive_trace(trace, title=\"Check your n=8 prediction\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- Which stage feels easiest to see by eye?\n- Which stage feels most like \u201cpure schedule\u201d rather than \u201cnew algebra\u201d?\n- Why is BO output not a bug?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Change The n=8 Signal" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Change The n=8 Signal\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi\nfrom ntt_learning.visuals import interactive_trace\n\npsi = find_psi(8, 97)\n\ndef preview(a0=0, a1=1, a2=2, a3=3, a4=4, a5=5, a6=6, a7=7):\n trace = fast_ntt_psi_ct_trace([a0, a1, a2, a3, a4, a5, a6, a7], 97, psi)\n display(interactive_trace(trace, title=\"Interactive n=8 CT trace\"))\n\ndisplay(\n widgets.interact(\n preview,\n a0=(0, 12),\n a1=(0, 12),\n a2=(0, 12),\n a3=(0, 12),\n a4=(0, 12),\n a5=(0, 12),\n a6=(0, 12),\n a7=(0, 12),\n )\n)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `problems.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lab: Fast Forward CT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb b/notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb new file mode 100644 index 0000000..3789e91 --- /dev/null +++ b/notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Objectives" + } + }, + "source": "## META | difficulty 1 | Objectives\n\nThis bundle is where the transform stops being a full matrix multiplication and becomes a staged butterfly network.\n\nFocus:\n\n- CT as the fast forward NTT strategy\n- visible stage arrays\n- explicit zeta values per pair\n- BO output vs NO output\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "CT Is A Schedule For Reusing Work" + } + }, + "source": "## MANDATORY | difficulty 3 | CT Is A Schedule For Reusing Work\n\nThe point of the CT butterfly is not to invent a new transform.\nThe point is to compute the same transform by reusing shared bracket terms instead of recomputing everything from scratch.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Trace The Exact n=4 Paper Example" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Trace The Exact n=4 Paper Example\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, forward_ntt_psi\nfrom ntt_learning.visuals import interactive_trace, plot_trace_overview\n\nsignal = [1, 2, 3, 4]\nmodulus = 7681\npsi = 1925\ntrace = fast_ntt_psi_ct_trace(signal, modulus, psi)\n\nprint(\"raw CT output (BO):\", trace.raw_output)\nprint(\"bit-reversed back to NO:\", trace.normal_order_output)\nprint(\"direct NTT_psi:\", forward_ntt_psi(signal, modulus, psi))\ndisplay(plot_trace_overview(trace, title=\"CT overview for [1,2,3,4]\"))\ndisplay(interactive_trace(trace, title=\"CT forward trace\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "What You Should Notice In The Stage Viewer" + } + }, + "source": "## MANDATORY | difficulty 3 | What You Should Notice In The Stage Viewer\n\nDo not just read the final answer.\nNotice:\n\n- which pairs talk to each other in each stage\n- which `zeta` each pair uses\n- how the array order changes before the final bit-reversal correction\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Run The Second n=4 Paper Example" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Run The Second n=4 Paper Example\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace\nfrom ntt_learning.visuals import interactive_trace\n\nsignal = [5, 6, 7, 8]\ntrace = fast_ntt_psi_ct_trace(signal, 7681, 1925)\nprint(\"BO output:\", trace.raw_output)\nprint(\"NO output:\", trace.normal_order_output)\ndisplay(interactive_trace(trace, title=\"Second CT trace\"))\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Go One Stage Deeper With n=8" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Go One Stage Deeper With n=8\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi\nfrom ntt_learning.visuals import interactive_trace, plot_trace_overview\n\nsignal = [0, 1, 2, 3, 4, 5, 6, 7]\nmodulus = 97\npsi = find_psi(8, modulus)\ntrace = fast_ntt_psi_ct_trace(signal, modulus, psi)\n\nprint(\"psi:\", psi)\nprint(\"BO output:\", trace.raw_output)\nprint(\"NO output:\", trace.normal_order_output)\ndisplay(plot_trace_overview(trace, title=\"Three CT stages for n=8\"))\ndisplay(interactive_trace(trace, title=\"n=8 CT stage explorer\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Retrieval Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\n1. What does CT change: the transform definition or the computation schedule?\n2. Why is the output naturally in BO rather than NO?\n3. In a stage diagram, what are the first three things you should inspect before any formula?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Inspect Stage Rows As Data" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Inspect Stage Rows As Data\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, stage_rows\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\nfor stage in trace.stages:\n print(\"stage\", stage.stage_index)\n for row in stage_rows(stage):\n print(row)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `lab.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lecture: Fast Forward CT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb b/notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb new file mode 100644 index 0000000..38f858c --- /dev/null +++ b/notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Problem Set Goals" + } + }, + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the CT schedule is now a visible object rather than a name.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Multiple-Choice Retrieval" + } + }, + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. CT makes the transform faster by:\n A. changing the ring\n B. reusing bracket structure through staged butterflies\n C. deleting the inverse\n\n2. The natural output order of the direct CT schedule discussed here is:\n A. normal order\n B. bit-reversed order\n C. random order\n\n3. A stage explorer is useful mainly because it:\n A. hides the pairings\n B. makes data movement and zeta usage inspectable\n C. removes the need for examples\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Answer Key" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"B\", 3: \"B\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Paper Example Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Paper Example Check\n\nVerify that the CT trace on `[1,2,3,4]` in `Z_7681` with `\u03c8 = 1925` lands on the paper\u2019s BO output.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Check The BO Output" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Check The BO Output\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\nprint(trace.raw_output)\nassert list(trace.raw_output) == [1467, 3471, 2807, 7621]\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Written Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nExplain why the phrase \u201csame transform, better schedule\u201d is the right headline for CT.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional Challenge" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi\n\ntrace = fast_ntt_psi_ct_trace([3, 1, 4, 1, 5, 9, 2, 6], 97, find_psi(8, 97))\nfor stage in trace.stages:\n print(stage.stage_index, stage.output_values)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `studio.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Problems: Fast Forward CT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb b/notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb new file mode 100644 index 0000000..1a3fd07 --- /dev/null +++ b/notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Studio Goals" + } + }, + "source": "## META | difficulty 1 | Studio Goals\n\nThis studio compares CT traces and inspects where learners usually lose the plot: stage order, pair order, and BO output.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Same Schedule, Different Signals" + } + }, + "source": "## MANDATORY | difficulty 3 | Same Schedule, Different Signals\n\nThe butterfly pattern is structural.\nThe signal values change, but the pairing pattern and the zeta schedule stay tied to `n`, the modulus, and the chosen root.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Compare Two CT Traces" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Compare Two CT Traces\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace\nfrom ntt_learning.visuals import plot_trace_overview\n\ntrace_a = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\ntrace_b = fast_ntt_psi_ct_trace([5, 6, 7, 8], 7681, 1925)\n\ndisplay(plot_trace_overview(trace_a, title=\"CT trace A\"))\ndisplay(plot_trace_overview(trace_b, title=\"CT trace B\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Debug Checklist" + } + }, + "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nIf a CT implementation looks wrong, inspect:\n\n1. the chosen `\u03c8`\n2. the stage pairings\n3. the zeta exponent sequence\n4. whether you remembered the final BO -> NO reorder when comparing against the direct transform\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "See A Wrong-Order Comparison Failure" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | See A Wrong-Order Comparison Failure\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, forward_ntt_psi\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\ndirect = forward_ntt_psi([1, 2, 3, 4], 7681, 1925)\n\nprint(\"wrong comparison: CT BO output vs direct NO output\")\nprint(trace.raw_output, direct)\nprint(\"correct comparison: CT NO output vs direct NO output\")\nprint(trace.normal_order_output, direct)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nIn one paragraph, explain why a learner can understand the direct transform and still get lost in CT if the stage order and output order are not shown visually.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Inspect The zeta schedule" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Inspect The zeta schedule\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\nfor stage in trace.stages:\n print(\"stage\", stage.stage_index, \"zetas:\", stage.zetas)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Studio: Fast Forward CT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb b/notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb new file mode 100644 index 0000000..df4e3a5 --- /dev/null +++ b/notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Lab Goals" + } + }, + "source": "## META | difficulty 1 | Lab Goals\n\nPredict the BO input and the final scaling before you run the stage explorer.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 1" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 1\n\nExplain before running the next cell:\n\n- why the GS input must be BO in the standard schedule\n- why the unscaled output is not yet the original signal\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\nfrom ntt_learning.visuals import interactive_trace\n\ntrace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925)\ndisplay(interactive_trace(trace, title=\"Check your GS prediction\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 2" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 2\n\nPredict the bit-reversal of `[0,1,2,3,4,5,6,7]` before running the next cell.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check For Ordering" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check For Ordering\n\nfrom ntt_learning.toy_ntt import bit_reversed_order\n\nprint(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7]))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- Which part of GS felt like a true inverse to you?\n- Which part felt like pure bookkeeping?\n- Why is the ordering story impossible to ignore?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Explore Another BO Input" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Explore Another BO Input\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\nfrom ntt_learning.visuals import interactive_trace\n\ndef preview(x0=1467, x1=3471, x2=2807, x3=7621):\n trace = fast_intt_psi_gs_trace([x0, x1, x2, x3], 7681, 1925)\n display(interactive_trace(trace, title=\"Interactive GS trace\"))\n\ndisplay(\n widgets.interact(\n preview,\n x0=(0, 7680),\n x1=(0, 7680),\n x2=(0, 7680),\n x3=(0, 7680),\n )\n)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `problems.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lab: Fast Inverse GS", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb b/notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb new file mode 100644 index 0000000..60915eb --- /dev/null +++ b/notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Objectives" + } + }, + "source": "## META | difficulty 1 | Objectives\n\nThis bundle makes the inverse flow explicit.\n\nFocus:\n\n- GS as the fast inverse schedule\n- BO input and NO output\n- why the final `n^-1` scaling appears\n- how bit-reversal fits the forward/inverse pair\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "GS Feels Like The Same Network Seen From The Other End" + } + }, + "source": "## MANDATORY | difficulty 3 | GS Feels Like The Same Network Seen From The Other End\n\nThe inverse is not \u201cmysterious undoing\u201d.\nIt is a staged network with the same family resemblance as CT, but with a different direction of arithmetic and a final scaling.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Trace The Exact n=4 GS Paper Example" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Trace The Exact n=4 GS Paper Example\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\nfrom ntt_learning.visuals import interactive_trace, plot_trace_overview\n\nbo_input = [1467, 3471, 2807, 7621]\ntrace = fast_intt_psi_gs_trace(bo_input, 7681, 1925)\n\nprint(\"unscaled NO output:\", trace.raw_output)\nprint(\"scaled NO output:\", trace.scaled_output)\ndisplay(plot_trace_overview(trace, title=\"GS overview for the n=4 paper example\"))\ndisplay(interactive_trace(trace, title=\"GS inverse trace\"))\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "See The Bit-Reversal Map Explicitly" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | See The Bit-Reversal Map Explicitly\n\nfrom IPython.display import display\n\nfrom ntt_learning.visuals import plot_bit_reversal_mapping\n\ndisplay(plot_bit_reversal_mapping(4, title=\"Bit-reversal for n=4\"))\ndisplay(plot_bit_reversal_mapping(8, title=\"Bit-reversal for n=8\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Why Scaling Waits Until The End" + } + }, + "source": "## MANDATORY | difficulty 3 | Why Scaling Waits Until The End\n\nEach GS stage avoids local division by `2`.\nThe accumulated effect of those missing local divisions is corrected by the final multiplication with `n^-1`.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Full Forward And Inverse Round Trip" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Full Forward And Inverse Round Trip\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace\n\nsignal = [1, 2, 3, 4]\nforward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925)\n\nprint(\"forward BO output:\", forward_trace.raw_output)\nprint(\"inverse scaled output:\", inverse_trace.scaled_output)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Retrieval Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\n1. Why does GS want BO input?\n2. Why does the final scaling not disappear?\n3. What would go wrong if you visually compared GS input and CT NO output without respecting the order change?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: A Second GS Example" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: A Second GS Example\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\nfrom ntt_learning.visuals import interactive_trace\n\ntrace = fast_intt_psi_gs_trace([2489, 6478, 7489, 6607], 7681, 1925)\nprint(\"scaled output:\", trace.scaled_output)\ndisplay(interactive_trace(trace, title=\"Second GS trace\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `lab.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lecture: Fast Inverse GS", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb b/notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb new file mode 100644 index 0000000..5d91c6e --- /dev/null +++ b/notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Problem Set Goals" + } + }, + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the inverse flow, ordering, and scaling are now mechanically stable.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Multiple-Choice Retrieval" + } + }, + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. GS is used here as the fast schedule for:\n A. direct forward NTT\n B. inverse NTT\n C. schoolbook multiplication\n\n2. The final `n^-1` matters because:\n A. each stage skipped local divisions that accumulate\n B. the modulus changed mid-computation\n C. bit-reversal requires scaling\n\n3. In the standard pairing, GS expects:\n A. NO input and BO output\n B. BO input and NO output\n C. random order on both ends\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Answer Key" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"A\", 3: \"B\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Paper Example Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Paper Example Check\n\nVerify that the GS trace on the paper\u2019s BO input scales back to `[1,2,3,4]`.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Check The Final Scaling" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Check The Final Scaling\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\n\ntrace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925)\nprint(trace.scaled_output)\nassert list(trace.scaled_output) == [1, 2, 3, 4]\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Written Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nIn one paragraph, explain why \u201csame structure, opposite direction\u201d is a better intuition for GS than \u201ctotally different algorithm\u201d.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional Challenge" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\n\ntrace = fast_intt_psi_gs_trace([2489, 6478, 7489, 6607], 7681, 1925)\nfor stage in trace.stages:\n print(stage.stage_index, stage.output_values)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `studio.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Problems: Fast Inverse GS", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb b/notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb new file mode 100644 index 0000000..ac4dafe --- /dev/null +++ b/notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Studio Goals" + } + }, + "source": "## META | difficulty 1 | Studio Goals\n\nThe studio puts CT and GS next to each other and treats ordering as a first-class object, not a side note.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Forward And Inverse Need To Meet In The Middle Cleanly" + } + }, + "source": "## MANDATORY | difficulty 3 | Forward And Inverse Need To Meet In The Middle Cleanly\n\nThe whole point of the pair is:\n\n- CT gets you into the transform domain efficiently\n- GS gets you back out efficiently\n- the two only meet cleanly if you respect BO/NO and the final scaling\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "See CT Output Feed GS Input" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | See CT Output Feed GS Input\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace\n\nsignal = [5, 6, 7, 8]\nforward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925)\n\nprint(\"CT BO output:\", forward_trace.raw_output)\nprint(\"GS scaled output:\", inverse_trace.scaled_output)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Debug Checklist" + } + }, + "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nIf the inverse output is wrong, inspect:\n\n1. whether the input was BO\n2. whether the zetas were inverse-stage zetas\n3. whether the final `n^-1` scaling was applied\n4. whether you compared the correct order against the direct reference\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "See A Missing-Scale Failure" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | See A Missing-Scale Failure\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace\n\ntrace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925)\nprint(\"unscaled:\", trace.raw_output)\nprint(\"scaled:\", trace.scaled_output)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nExplain why \u201cthe inverse looked almost right\u201d is a dangerous debugging sentence unless you say what happened with ordering and scaling.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Another bit-reversal map" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Another bit-reversal map\n\nfrom IPython.display import display\n\nfrom ntt_learning.visuals import plot_bit_reversal_mapping\n\ndisplay(plot_bit_reversal_mapping(16, title=\"Bit-reversal for n=16\"))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Studio: Fast Inverse GS", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb b/notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb index 2cb0a30..cc50729 100644 --- a/notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb +++ b/notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb @@ -10,7 +10,7 @@ "title": "Lab Goals" } }, - "source": "## META | difficulty 1 | Lab Goals\n\nThis lab asks for prediction before execution.\n\nThe learner should pause and name the expected pairings and sign changes before reading the output.\n" + "source": "## META | difficulty 1 | Lab Goals\n\nPredict the movement before you run the code.\n\nThe point is not just to see the picture after the fact.\nThe point is to force your eye to anticipate where the products and wraparound terms will land.\n" }, { "cell_type": "markdown", @@ -22,7 +22,7 @@ "title": "Exercise 1" } }, - "source": "## MANDATORY | difficulty 2 | Exercise 1\n\nBefore running the next cell, predict which coefficients will collide when the raw convolution is folded into `x^4 + 1`.\n" + "source": "## MANDATORY | difficulty 2 | Exercise 1\n\nBefore running the next cell:\n\n- name the diagonal sums of the raw schoolbook grid\n- say which tail terms will wrap into slot `0`\n- say whether they add or subtract in `x^4 + 1`\n" }, { "cell_type": "code", @@ -32,23 +32,23 @@ "role": "mandatory", "difficulty": 2, "kind": "exercise", - "title": "Work Two Multiplication Examples" + "title": "Run The Prediction Check" } }, "outputs": [], - "source": "# MANDATORY | difficulty 2 | Work Two Multiplication Examples\n\nfrom ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution\n\nsamples = [\n ([1, 2, 0, 0], [3, 4, 0, 0]),\n ([5, 0, 1, 2], [2, 1, 0, 1]),\n]\n\nfor left, right in samples:\n print(\"left:\", left, \"right:\", right)\n print(\" convolution:\", schoolbook_convolution(left, right))\n print(\" negacyclic:\", negacyclic_multiply(left, right, n=4))\n" + "source": "# MANDATORY | difficulty 2 | Run The Prediction Check\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution\nfrom ntt_learning.visuals import plot_convolution_grid, plot_wraparound\n\nleft = [3, 0, 2, 1]\nright = [1, 4, 0, 2]\nraw = schoolbook_convolution(left, right)\n\nprint(\"raw convolution:\", raw)\nprint(\"negacyclic result:\", negacyclic_multiply(left, right, n=4))\ndisplay(plot_convolution_grid(left, right, title=\"Prediction check grid\"))\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"Prediction check fold\"))\n" }, { "cell_type": "markdown", "metadata": { "pedagogy": { "role": "mandatory", - "difficulty": 3, + "difficulty": 2, "kind": "exercise", "title": "Exercise 2" } }, - "source": "## MANDATORY | difficulty 3 | Exercise 2\n\nPredict the pairings for a single Cooley-Tukey stage on eight values with block size four.\nName the index pairs before running the cell.\n" + "source": "## MANDATORY | difficulty 2 | Exercise 2\n\nPick one number in the raw tail and follow it all the way to its final slot.\nDo not say \u201cit wraps around\u201d.\nSay exactly:\n\n- where it started\n- how many wraps happened\n- whether the sign flipped\n- where it finished\n" }, { "cell_type": "code", @@ -56,13 +56,13 @@ "metadata": { "pedagogy": { "role": "mandatory", - "difficulty": 3, + "difficulty": 2, "kind": "exercise", - "title": "Trace One Butterfly Layer" + "title": "A Second Visual Drill" } }, "outputs": [], - "source": "# MANDATORY | difficulty 3 | Trace One Butterfly Layer\n\nfrom ntt_learning.toy_ntt import action_rows, apply_ct_stage\n\nvalues = [0, 1, 2, 3, 4, 5, 6, 7]\nstage_output, stage_actions = apply_ct_stage(\n values,\n block_size=4,\n zetas=[1, 4, 1, 4],\n modulus=17,\n)\n\nprint(\"stage output:\", stage_output)\nfor row in action_rows(stage_actions):\n print(row)\n" + "source": "# MANDATORY | difficulty 2 | A Second Visual Drill\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import schoolbook_convolution\nfrom ntt_learning.visuals import plot_wraparound\n\nraw = schoolbook_convolution([2, 5, 0, 1], [1, 0, 3, 2])\nprint(\"raw convolution:\", raw)\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"Trace one tail coefficient by eye\"))\n" }, { "cell_type": "markdown", @@ -74,7 +74,7 @@ "title": "Reflection" } }, - "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- Which part of the stage felt mechanical and local?\n- Which part still feels global or mysterious?\n- If one zeta is wrong, what kind of output difference would you expect to see?\n" + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- What felt easier to see in the grid than in symbolic polynomial notation?\n- What exactly makes negacyclic folding more annoying than ordinary wraparound?\n- If you had to explain `x^n + 1` to somebody visually, what would you draw?\n" }, { "cell_type": "code", @@ -84,11 +84,11 @@ "role": "facultative", "difficulty": 4, "kind": "exploration", - "title": "Optional Inverse-Style Stage" + "title": "Optional: Try Your Own Arrays" } }, "outputs": [], - "source": "# FACULTATIVE | difficulty 4 | Optional Inverse-Style Stage\n\nfrom ntt_learning.toy_ntt import action_rows, apply_gs_stage\n\nvalues = [5, 1, 9, 3, 7, 2, 6, 4]\nstage_output, stage_actions = apply_gs_stage(\n values,\n block_size=4,\n zetas=[1, 4, 1, 4],\n modulus=17,\n)\n\nprint(\"stage output:\", stage_output)\nfor row in action_rows(stage_actions):\n print(row)\n" + "source": "# FACULTATIVE | difficulty 4 | Optional: Try Your Own Arrays\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import schoolbook_convolution\nfrom ntt_learning.visuals import plot_convolution_grid, plot_wraparound\n\ndef preview(a0=1, a1=2, a2=3, a3=4, b0=5, b1=6, b2=7, b3=8):\n left = [a0, a1, a2, a3]\n right = [b0, b1, b2, b3]\n raw = schoolbook_convolution(left, right)\n display(plot_convolution_grid(left, right, title=\"Interactive schoolbook grid\"))\n display(plot_wraparound(raw, n=4, negacyclic=True, title=\"Interactive negacyclic fold\"))\n\ndisplay(\n widgets.interact(\n preview,\n a0=(0, 6),\n a1=(0, 6),\n a2=(0, 6),\n a3=(0, 6),\n b0=(0, 6),\n b1=(0, 6),\n b2=(0, 6),\n b3=(0, 6),\n )\n)\n" }, { "cell_type": "markdown", @@ -114,14 +114,35 @@ }, "ntt_learning": { "title": "Lab: Convolution To Toy NTT", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb b/notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb index c8f31bd..3b5ab81 100644 --- a/notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb +++ b/notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb @@ -10,7 +10,7 @@ "title": "Objectives" } }, - "source": "## META | difficulty 1 | Objectives\n\nThis notebook introduces the first technical bundle.\n\nFocus:\n\n- why polynomial multiplication matters\n- what negacyclic folding changes\n- how a tiny toy NTT gives a matrix-level view\n- what a butterfly does locally\n" + "source": "## META | difficulty 1 | Objectives\n\nThis first bundle is about making the raw multiplication problem visible before any transform is introduced.\n\nFocus:\n\n- the schoolbook product grid\n- diagonal sums\n- cyclic vs negacyclic folding\n- a tiny teaser of why transforms help\n" }, { "cell_type": "markdown", @@ -19,10 +19,10 @@ "role": "mandatory", "difficulty": 2, "kind": "explanation", - "title": "Convolution Before Transforms" + "title": "Schoolbook Multiplication Is A Grid" } }, - "source": "## MANDATORY | difficulty 2 | Convolution Before Transforms\n\nStart with the concrete problem. Two coefficient arrays multiply by accumulating all pairwise products.\nThat schoolbook view is the baseline the learner should be able to inspect by hand before a transform is introduced.\n" + "source": "## MANDATORY | difficulty 2 | Schoolbook Multiplication Is A Grid\n\nStop thinking \u201cmultiply two polynomials\u201d as one sentence.\nThe mechanical reality is a grid of pairwise products whose diagonals have to be accumulated.\n\nIf that grid is not concrete, the NTT has nothing to optimize in your mind.\n" }, { "cell_type": "code", @@ -32,11 +32,11 @@ "role": "mandatory", "difficulty": 2, "kind": "demo", - "title": "Inspect Convolution And Negacyclic Folding" + "title": "See The Product Grid And The Diagonal Sums" } }, "outputs": [], - "source": "# MANDATORY | difficulty 2 | Inspect Convolution And Negacyclic Folding\n\nfrom ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution\n\nleft = [2, 1, 3, 0]\nright = [1, 4, 0, 2]\n\nprint(\"convolution:\", schoolbook_convolution(left, right))\nprint(\"negacyclic in x^4 + 1:\", negacyclic_multiply(left, right, n=4))\n" + "source": "# MANDATORY | difficulty 2 | See The Product Grid And The Diagonal Sums\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import convolution_contributions, schoolbook_convolution\nfrom ntt_learning.visuals import plot_convolution_grid\n\nleft = [1, 2, 3, 4]\nright = [5, 6, 7, 8]\nraw = schoolbook_convolution(left, right)\n\nprint(\"raw convolution:\", raw)\nfor row in convolution_contributions(left, right):\n print(row)\n\nfig = plot_convolution_grid(left, right, title=\"Schoolbook products for [1,2,3,4] * [5,6,7,8]\")\ndisplay(fig)\n" }, { "cell_type": "markdown", @@ -45,10 +45,10 @@ "role": "mandatory", "difficulty": 2, "kind": "explanation", - "title": "Toy NTT As A Round Trip" + "title": "Wraparound Is The First Structural Fork" } }, - "source": "## MANDATORY | difficulty 2 | Toy NTT As A Round Trip\n\nA tiny transform is useful because it keeps every entry inspectable. The first goal is not Kyber fidelity.\nThe first goal is to see that the transform maps one coefficient view to another and can be inverted.\n" + "source": "## MANDATORY | difficulty 2 | Wraparound Is The First Structural Fork\n\nOnce the raw tail exists, the ring tells you what to do with it.\n\n- in `x^n - 1`, high-degree terms wrap back with a positive sign\n- in `x^n + 1`, high-degree terms wrap back with a sign flip\n\nThat sign flip is not cosmetic. It is exactly what makes the negacyclic story different.\n" }, { "cell_type": "code", @@ -58,37 +58,23 @@ "role": "mandatory", "difficulty": 2, "kind": "demo", - "title": "Run A Tiny Forward And Inverse NTT" + "title": "See Cyclic And Negacyclic Folding Side By Side" } }, "outputs": [], - "source": "# MANDATORY | difficulty 2 | Run A Tiny Forward And Inverse NTT\n\nfrom ntt_learning.toy_ntt import find_primitive_root, forward_ntt, inverse_ntt\n\nmodulus = 17\nomega = find_primitive_root(order=4, modulus=modulus)\nsignal = [3, 1, 4, 1]\nspectrum = forward_ntt(signal, modulus=modulus, omega=omega)\n\nprint(\"primitive 4th root:\", omega)\nprint(\"forward spectrum:\", spectrum)\nprint(\"inverse recovery:\", inverse_ntt(spectrum, modulus=modulus, omega=omega))\n" + "source": "# MANDATORY | difficulty 2 | See Cyclic And Negacyclic Folding Side By Side\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution, wraparound_contributions\nfrom ntt_learning.visuals import plot_wraparound\n\nleft = [1, 2, 3, 4]\nright = [5, 6, 7, 8]\nraw = schoolbook_convolution(left, right)\n\nprint(\"raw convolution:\", raw)\nprint(\"negacyclic in x^4 + 1:\", negacyclic_multiply(left, right, n=4))\nprint(\"cyclic folding rows:\")\nfor row in wraparound_contributions(raw, n=4, negacyclic=False):\n print(row)\nprint(\"negacyclic folding rows:\")\nfor row in wraparound_contributions(raw, n=4, negacyclic=True):\n print(row)\n\ndisplay(plot_wraparound(raw, n=4, negacyclic=False, title=\"Cyclic folding into x^4 - 1\"))\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"Negacyclic folding into x^4 + 1\"))\n" }, { "cell_type": "markdown", "metadata": { "pedagogy": { "role": "mandatory", - "difficulty": 3, + "difficulty": 2, "kind": "explanation", - "title": "Butterflies Are Local Dataflow" + "title": "The Tiny Transform Teaser" } }, - "source": "## MANDATORY | difficulty 3 | Butterflies Are Local Dataflow\n\nA butterfly is a local rewrite of a pair. The pair changes because one branch is twiddled by a zeta value.\nThis is separate from the global story about polynomial multiplication.\n\nForward Cooley-Tukey and inverse Gentleman-Sande have the same shape intuition: pair values, combine them, and move layer by layer.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pedagogy": { - "role": "mandatory", - "difficulty": 3, - "kind": "demo", - "title": "Compare Pairwise And Stage-Level Butterfly Views" - } - }, - "outputs": [], - "source": "# MANDATORY | difficulty 3 | Compare Pairwise And Stage-Level Butterfly Views\n\nfrom ntt_learning.toy_ntt import action_rows, apply_ct_stage, ct_butterfly_pair, gs_butterfly_pair\n\nprint(\"single CT pair:\", ct_butterfly_pair(top=7, bottom=5, zeta=3, modulus=17))\nprint(\"single GS pair:\", gs_butterfly_pair(top=7, bottom=5, zeta=3, modulus=17))\n\nvalues = [3, 1, 4, 1]\nstage_output, stage_actions = apply_ct_stage(values, block_size=2, zetas=1, modulus=17)\n\nprint(\"stage output:\", stage_output)\nprint(\"stage trace:\", action_rows(stage_actions))\n" + "source": "## MANDATORY | difficulty 2 | The Tiny Transform Teaser\n\nThe transform is not magic. It is a change of coordinates chosen so that multiplication gets easier.\n\nThe next bundle will treat the transform itself directly.\nThis first bundle only makes sure the learner can see the raw thing being optimized.\n" }, { "cell_type": "markdown", @@ -100,19 +86,7 @@ "title": "Retrieval Check" } }, - "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\nQuiz:\n\n1. What changes when schoolbook multiplication is folded negacyclically?\n2. Why is the toy NTT introduced before Kyber indexing details?\n3. In a butterfly, which part of the computation is local and directly inspectable?\n" - }, - { - "cell_type": "markdown", - "metadata": { - "pedagogy": { - "role": "facultative", - "difficulty": 4, - "kind": "exploration", - "title": "Optional Extension" - } - }, - "source": "## FACULTATIVE | difficulty 4 | Optional Extension\n\nIf the local pairings already feel comfortable, inspect a bit-reversed ordering next. That prepares the learner\nfor later discussions of array ordering without mixing it into the mandatory route too early.\n" + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\nAnswer in words before moving on:\n\n1. Why do diagonal sums appear in schoolbook multiplication?\n2. What is the one exact sign difference between cyclic and negacyclic folding?\n3. If you cannot track the tail wraparound, what part of the NTT story will stay vague?\n" }, { "cell_type": "code", @@ -122,11 +96,11 @@ "role": "facultative", "difficulty": 4, "kind": "exploration", - "title": "Bit-Reversed Ordering" + "title": "Optional: Compare Another Example" } }, "outputs": [], - "source": "# FACULTATIVE | difficulty 4 | Bit-Reversed Ordering\n\nfrom ntt_learning.toy_ntt import bit_reversed_order\n\nprint(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7]))\n" + "source": "# FACULTATIVE | difficulty 4 | Optional: Compare Another Example\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution\nfrom ntt_learning.visuals import plot_convolution_grid, plot_wraparound\n\nleft = [2, 1, 0, 3]\nright = [4, 0, 1, 2]\nraw = schoolbook_convolution(left, right)\n\nprint(\"raw convolution:\", raw)\nprint(\"negacyclic:\", negacyclic_multiply(left, right, n=4))\ndisplay(plot_convolution_grid(left, right, title=\"A second schoolbook grid\"))\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"A second negacyclic fold\"))\n" }, { "cell_type": "markdown", @@ -152,14 +126,35 @@ }, "ntt_learning": { "title": "Lecture: Convolution To Toy NTT", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb b/notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb index 0592e4a..ee92c18 100644 --- a/notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb +++ b/notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb @@ -10,7 +10,7 @@ "title": "Problem Set Goals" } }, - "source": "## META | difficulty 1 | Problem Set Goals\n\nUse this notebook to check retrieval, not to discover the topic for the first time.\nIf the questions feel opaque, return to the lecture and lab first.\n" + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the multiplication and folding pictures are now stable in memory.\n" }, { "cell_type": "markdown", @@ -22,7 +22,7 @@ "title": "Multiple-Choice Retrieval" } }, - "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. Negacyclic reduction mainly changes:\n A. coefficient labels only\n B. wraparound terms by folding them back with sign changes\n C. the modulus but not the polynomial ring\n\n2. The toy NTT is introduced early because it:\n A. already matches Kyber implementation details exactly\n B. removes the need to inspect arrays\n C. gives a small, reversible transform that can be inspected directly\n\n3. A butterfly stage is best thought of as:\n A. a local rewrite over paired entries\n B. a proof that convolution is impossible\n C. a random permutation with no arithmetic structure\n" + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. The diagonal sums in schoolbook multiplication come from:\n A. random coincidence\n B. grouping terms with the same final degree\n C. bit-reversal\n\n2. Negacyclic folding differs from cyclic folding because:\n A. the wrapped tail flips sign\n B. the polynomial degrees disappear\n C. the raw convolution gets shorter before folding\n\n3. The main reason to study the raw grid before NTT is:\n A. because the transform is impossible otherwise\n B. because it makes the optimized algorithm visually grounded\n C. because Kyber never uses transforms\n" }, { "cell_type": "code", @@ -36,7 +36,33 @@ } }, "outputs": [], - "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {\n 1: \"B\",\n 2: \"C\",\n 3: \"A\",\n}\n\nprint(answers)\n" + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"A\", 3: \"B\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Manual Fold Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Manual Fold Check\n\nCompute the negacyclic fold of the raw vector `[5, 16, 34, 60, 61, 52, 32]` into `x^4 + 1` by hand before running the next cell.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Check The Fold" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Check The Fold\n\nfrom ntt_learning.toy_ntt import negacyclic_reduce\n\nraw = [5, 16, 34, 60, 61, 52, 32]\nprint(negacyclic_reduce(raw, n=4))\n" }, { "cell_type": "markdown", @@ -48,24 +74,11 @@ "title": "Written Reflection" } }, - "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nReflection prompt:\n\n- In one paragraph, separate the algebraic purpose of the NTT from the local butterfly dataflow.\n- In one sentence, explain why the course postpones Kyber-specific indexing.\n" + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nIn one paragraph, explain why \u201cwrap the tail back\u201d is still too vague unless you also specify:\n\n- the divisor\n- the target slot\n- the sign rule\n" }, { "cell_type": "code", "execution_count": null, - "metadata": { - "pedagogy": { - "role": "mandatory", - "difficulty": 2, - "kind": "exercise", - "title": "Verify A Round Trip" - } - }, - "outputs": [], - "source": "# MANDATORY | difficulty 2 | Verify A Round Trip\n\nfrom ntt_learning.toy_ntt import find_primitive_root, forward_ntt, inverse_ntt\n\nsignal = [3, 1, 4, 1]\nmodulus = 17\nomega = find_primitive_root(order=4, modulus=modulus)\nrecovered = inverse_ntt(forward_ntt(signal, modulus=modulus, omega=omega), modulus=modulus, omega=omega)\n\nassert recovered == signal\nprint(\"round-trip verified:\", recovered)\n" - }, - { - "cell_type": "markdown", "metadata": { "pedagogy": { "role": "facultative", @@ -74,21 +87,8 @@ "title": "Optional Challenge" } }, - "source": "## FACULTATIVE | difficulty 4 | Optional Challenge\n\nTry replacing the signal with your own four coefficients and predict the forward spectrum before running the next cell.\n" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pedagogy": { - "role": "facultative", - "difficulty": 4, - "kind": "exploration", - "title": "Explore Another Signal" - } - }, "outputs": [], - "source": "# FACULTATIVE | difficulty 4 | Explore Another Signal\n\nfrom ntt_learning.toy_ntt import find_primitive_root, forward_ntt\n\nsignal = [6, 0, 5, 2]\nmodulus = 17\nomega = find_primitive_root(order=4, modulus=modulus)\n\nprint(\"spectrum:\", forward_ntt(signal, modulus=modulus, omega=omega))\n" + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nfrom ntt_learning.toy_ntt import wraparound_contributions\n\nraw = [3, 11, 7, 0, 5, 9, 4]\nfor row in wraparound_contributions(raw, n=4, negacyclic=True):\n print(row)\n" }, { "cell_type": "markdown", @@ -114,14 +114,35 @@ }, "ntt_learning": { "title": "Problems: Convolution To Toy NTT", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb b/notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb index 564874f..60dd26b 100644 --- a/notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb +++ b/notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb @@ -10,7 +10,7 @@ "title": "Studio Goals" } }, - "source": "## META | difficulty 1 | Studio Goals\n\nThis studio frames implementation reading.\n\nKeep three lenses separate:\n\n- algebraic purpose\n- array dataflow\n- protocol-specific conventions\n" + "source": "## META | difficulty 1 | Studio Goals\n\nThis studio is about comparison and diagnosis.\nThe learner should leave with a strong visual distinction between cyclic and negacyclic wraparound.\n" }, { "cell_type": "markdown", @@ -19,10 +19,10 @@ "role": "mandatory", "difficulty": 3, "kind": "explanation", - "title": "Forward And Inverse Flow Side By Side" + "title": "Two Folds, Same Raw Tail, Different Result" } }, - "source": "## MANDATORY | difficulty 3 | Forward And Inverse Flow Side By Side\n\nThe goal here is not to claim the same cell-by-cell formula for both directions.\nThe goal is to compare the same pairing structure while noticing that forward and inverse flows push arithmetic in opposite directions.\n" + "source": "## MANDATORY | difficulty 3 | Two Folds, Same Raw Tail, Different Result\n\nIf the raw convolution is fixed, the only thing that changes is the ring rule.\nThat is exactly why the same tail can produce two different reduced polynomials.\n" }, { "cell_type": "code", @@ -32,11 +32,11 @@ "role": "mandatory", "difficulty": 3, "kind": "demo", - "title": "Compare Cooley-Tukey And Gentleman-Sande Views" + "title": "Compare The Two Fold Rules" } }, "outputs": [], - "source": "# MANDATORY | difficulty 3 | Compare Cooley-Tukey And Gentleman-Sande Views\n\nfrom ntt_learning.toy_ntt import action_rows, apply_ct_stage, apply_gs_stage\n\nvalues = [2, 5, 7, 1, 3, 6, 4, 0]\nct_output, ct_actions = apply_ct_stage(values, block_size=4, zetas=[1, 4, 1, 4], modulus=17)\ngs_output, gs_actions = apply_gs_stage(values, block_size=4, zetas=[1, 4, 1, 4], modulus=17)\n\nprint(\"input:\", values)\nprint(\"ct output:\", ct_output)\nprint(\"gs output:\", gs_output)\nprint(\"ct trace:\", action_rows(ct_actions))\nprint(\"gs trace:\", action_rows(gs_actions))\n" + "source": "# MANDATORY | difficulty 3 | Compare The Two Fold Rules\n\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import schoolbook_convolution\nfrom ntt_learning.visuals import plot_wraparound\n\nraw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8])\nprint(\"raw convolution:\", raw)\ndisplay(plot_wraparound(raw, n=4, negacyclic=False, title=\"Positive wrap into x^4 - 1\"))\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"Negative wrap into x^4 + 1\"))\n" }, { "cell_type": "markdown", @@ -48,7 +48,7 @@ "title": "Debug Checklist" } }, - "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nWhen a stage looks wrong, inspect these in order:\n\n1. wrong pairings\n2. wrong zeta value\n3. wrong sign in the subtraction branch\n4. wrong direction choice between forward-style and inverse-style flow\n" + "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nIf a wraparound result looks wrong, inspect these in order:\n\n1. Was the raw convolution itself correct?\n2. Was the divisor `x^n - 1` or `x^n + 1`?\n3. Did the wrapped tail land in the right slot?\n4. Did the sign flip happen on the wrapped term?\n" }, { "cell_type": "code", @@ -58,23 +58,23 @@ "role": "mandatory", "difficulty": 2, "kind": "exercise", - "title": "Compare Baseline And Wrong-Zeta Output" + "title": "See A Wrong-Sign Failure" } }, "outputs": [], - "source": "# MANDATORY | difficulty 2 | Compare Baseline And Wrong-Zeta Output\n\nfrom ntt_learning.toy_ntt import apply_ct_stage\n\nvalues = [3, 1, 4, 1]\nbaseline, _ = apply_ct_stage(values, block_size=2, zetas=1, modulus=17)\nwrong_zeta, _ = apply_ct_stage(values, block_size=2, zetas=3, modulus=17)\n\nprint(\"baseline:\", baseline)\nprint(\"wrong zeta:\", wrong_zeta)\n" + "source": "# MANDATORY | difficulty 2 | See A Wrong-Sign Failure\n\nfrom ntt_learning.toy_ntt import schoolbook_convolution, negacyclic_reduce\n\nraw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8])\nwrong = [raw[0] + raw[4], raw[1] + raw[5], raw[2] + raw[6], raw[3]]\n\nprint(\"raw:\", raw)\nprint(\"wrong sign fold:\", wrong)\nprint(\"correct negacyclic fold:\", negacyclic_reduce(raw, n=4))\n" }, { "cell_type": "markdown", "metadata": { "pedagogy": { - "role": "facultative", - "difficulty": 4, - "kind": "exploration", - "title": "Optional Ordering Preview" + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" } }, - "source": "## FACULTATIVE | difficulty 4 | Optional Ordering Preview\n\nBit-reversal is important later, but it is deliberately optional here so the main route can stay focused on pair mechanics first.\n" + "source": "## MANDATORY | difficulty 2 | Reflection\n\nExplain the exact visual difference between \u201cthe wrong-sign fold\u201d and \u201cthe correct negacyclic fold\u201d.\n" }, { "cell_type": "code", @@ -84,11 +84,11 @@ "role": "facultative", "difficulty": 4, "kind": "exploration", - "title": "Inspect Bit-Reversed Order" + "title": "Optional: Fold A Larger Tail" } }, "outputs": [], - "source": "# FACULTATIVE | difficulty 4 | Inspect Bit-Reversed Order\n\nfrom ntt_learning.toy_ntt import bit_reversed_order\n\nprint(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7]))\n" + "source": "# FACULTATIVE | difficulty 4 | Optional: Fold A Larger Tail\n\nfrom IPython.display import display\n\nfrom ntt_learning.visuals import plot_wraparound\n\nraw = [4, 8, 12, 16, 9, 5, 1, 7, 11]\ndisplay(plot_wraparound(raw, n=4, negacyclic=True, title=\"Longer tail, same fold rule\"))\n" }, { "cell_type": "markdown", @@ -100,7 +100,7 @@ "title": "Next Notebook" } }, - "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: return to `../../COURSE_BLUEPRINT.ipynb` and extend the course into Kyber-specific notebooks.\n" + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../foundations/02_negative_wrapped_ntt/lecture.ipynb`\n" } ], "metadata": { @@ -114,14 +114,35 @@ }, "ntt_learning": { "title": "Studio: Convolution To Toy NTT", - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [ "notebooks/START_HERE.ipynb", "notebooks/COURSE_BLUEPRINT.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb" + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" ] } }, diff --git a/notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb b/notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb new file mode 100644 index 0000000..0956afb --- /dev/null +++ b/notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Lab Goals" + } + }, + "source": "## META | difficulty 1 | Lab Goals\n\nThe lab is about prediction inside the direct transform matrix.\nDo not run the next cells until you name the powers and products you expect to matter.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Exercise 1" + } + }, + "source": "## MANDATORY | difficulty 2 | Exercise 1\n\nFor `signal = [1,2,3,4]`, `n = 4`, `q = 17`, predict:\n\n- which powers of `\u03c8` appear in row `j = 1`\n- whether the inverse should need both `\u03c8^-1` and `n^-1`\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Prediction Check" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Prediction Check\n\nfrom ntt_learning.toy_ntt import find_psi, ntt_psi_exponent_grid\n\npsi = find_psi(4, 17)\nprint(\"psi:\", psi)\nfor row_index, row in enumerate(ntt_psi_exponent_grid(4)):\n print(\"row\", row_index, row)\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Interactive Signal Explorer" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Interactive Signal Explorer\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi\n\nmodulus = 17\npsi = find_psi(4, modulus)\n\ndef preview(a0=1, a1=2, a2=3, a3=4):\n signal = [a0, a1, a2, a3]\n spectrum = forward_ntt_psi(signal, modulus, psi)\n print(\"signal:\", signal)\n print(\"spectrum:\", spectrum)\n print(\"inverse:\", inverse_ntt_psi(spectrum, modulus, psi))\n\ndisplay(\n widgets.interact(\n preview,\n a0=(0, 16),\n a1=(0, 16),\n a2=(0, 16),\n a3=(0, 16),\n )\n)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Exercise 2" + } + }, + "source": "## MANDATORY | difficulty 2 | Exercise 2\n\nExplain what the pointwise multiplication in the transform domain is buying you.\nUse the words \u201creplace convolution by slotwise multiplication\u201d in your own sentence.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Compare Raw Convolution And Slotwise Multiplication" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Compare Raw Convolution And Slotwise Multiplication\n\nfrom ntt_learning.toy_ntt import (\n find_psi,\n forward_ntt_psi,\n inverse_ntt_psi,\n pointwise_multiply,\n schoolbook_convolution,\n)\n\nleft = [2, 1, 0, 3]\nright = [4, 0, 1, 2]\npsi = find_psi(4, 17)\nleft_hat = forward_ntt_psi(left, 17, psi)\nright_hat = forward_ntt_psi(right, 17, psi)\n\nprint(\"schoolbook raw:\", schoolbook_convolution(left, right))\nprint(\"left_hat:\", left_hat)\nprint(\"right_hat:\", right_hat)\nprint(\"pointwise product:\", pointwise_multiply(left_hat, right_hat, 17))\nprint(\"inverse:\", inverse_ntt_psi(pointwise_multiply(left_hat, right_hat, 17), 17, psi))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- What feels concrete in the direct transform matrix?\n- What still feels too expensive or repetitive?\n- Why are butterflies the obvious next step?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Try A Different Modulus" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Try A Different Modulus\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi\n\nmodulus = 97\npsi = find_psi(4, modulus)\nsignal = [1, 2, 3, 4]\n\nprint(\"modulus:\", modulus)\nprint(\"psi:\", psi)\nprint(\"spectrum:\", forward_ntt_psi(signal, modulus, psi))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `problems.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lab: Negative-Wrapped NTT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb b/notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb new file mode 100644 index 0000000..0bcfb2c --- /dev/null +++ b/notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Objectives" + } + }, + "source": "## META | difficulty 1 | Objectives\n\nThis bundle introduces the transform itself in its negacyclic form.\n\nFocus:\n\n- the difference between `\u03c9` and `\u03c8`\n- direct NTT\u03c8 and INTT\u03c8\n- the direct convolution theorem in the negacyclic setting\n- why this is still too slow at `O(n^2)` without butterflies\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "explanation", + "title": "Why \u03c8 Shows Up" + } + }, + "source": "## MANDATORY | difficulty 2 | Why \u03c8 Shows Up\n\nFor negative-wrapped convolution, the clean transform formula uses a `2n`-th root `\u03c8` with:\n\n- `\u03c8^2 = \u03c9`\n- `\u03c8^n = -1`\n\nThat is what bakes the negacyclic sign rule into the transform itself.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "demo", + "title": "Inspect \u03c9, \u03c8, And The Direct Transform Matrix" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Inspect \u03c9, \u03c8, And The Direct Transform Matrix\n\nfrom ntt_learning.toy_ntt import find_primitive_root, find_psi, ntt_psi_exponent_grid, ntt_psi_matrix\n\nmodulus = 17\nn = 4\nomega = find_primitive_root(n, modulus)\npsi = find_psi(n, modulus)\n\nprint(\"omega:\", omega)\nprint(\"psi:\", psi)\nprint(\"exponent grid:\")\nfor row in ntt_psi_exponent_grid(n):\n print(row)\nprint(\"NTT_psi matrix:\")\nfor row in ntt_psi_matrix(n, modulus, psi):\n print(row)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "explanation", + "title": "Direct NTT\u03c8 Is Mechanically Clear But Still Quadratic" + } + }, + "source": "## MANDATORY | difficulty 2 | Direct NTT\u03c8 Is Mechanically Clear But Still Quadratic\n\nThe direct transform is useful because every coefficient and every exponent is visible.\nIt is not yet efficient. It still performs the full matrix multiplication.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "demo", + "title": "Run A Direct NTT\u03c8 / INTT\u03c8 Round Trip" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Run A Direct NTT\u03c8 / INTT\u03c8 Round Trip\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi\n\nsignal = [1, 2, 3, 4]\nmodulus = 17\npsi = find_psi(len(signal), modulus)\nspectrum = forward_ntt_psi(signal, modulus, psi)\n\nprint(\"signal:\", signal)\nprint(\"spectrum:\", spectrum)\nprint(\"inverse recovery:\", inverse_ntt_psi(spectrum, modulus, psi))\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Use Direct NTT\u03c8 For Negacyclic Multiplication" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Use Direct NTT\u03c8 For Negacyclic Multiplication\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi, negacyclic_multiply, pointwise_multiply\n\nleft = [1, 2, 3, 4]\nright = [5, 6, 7, 8]\nmodulus = 17\npsi = find_psi(4, modulus)\n\nleft_hat = forward_ntt_psi(left, modulus, psi)\nright_hat = forward_ntt_psi(right, modulus, psi)\nproduct_hat = pointwise_multiply(left_hat, right_hat, modulus)\n\nprint(\"NTT_psi(left):\", left_hat)\nprint(\"NTT_psi(right):\", right_hat)\nprint(\"pointwise product:\", product_hat)\nprint(\"inverse of pointwise product:\", inverse_ntt_psi(product_hat, modulus, psi))\nprint(\"schoolbook negacyclic:\", negacyclic_multiply(left, right, n=4, modulus=modulus))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Retrieval Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\n1. Why is `\u03c8` stronger than `\u03c9` in the negacyclic story?\n2. What exact property does the inverse add that the forward transform does not?\n3. Why are we still dissatisfied after seeing the direct transform work correctly?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Compare Positive And Negative Wrapped Transforms" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Compare Positive And Negative Wrapped Transforms\n\nfrom ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt, forward_ntt_psi\n\nsignal = [1, 2, 3, 4]\nmodulus = 17\nomega = find_primitive_root(4, modulus)\npsi = find_psi(4, modulus)\n\nprint(\"positive-wrapped NTT:\", forward_ntt(signal, modulus, omega))\nprint(\"negative-wrapped NTT_psi:\", forward_ntt_psi(signal, modulus, psi))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `lab.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lecture: Negative-Wrapped NTT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb b/notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb new file mode 100644 index 0000000..d977812 --- /dev/null +++ b/notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Problem Set Goals" + } + }, + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the direct negative-wrapped transform is now mechanically understandable.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Multiple-Choice Retrieval" + } + }, + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. In the negacyclic transform, `\u03c8` matters because:\n A. it makes the modulus disappear\n B. it encodes the `x^n + 1` sign structure\n C. it avoids all inverses\n\n2. The inverse transform differs from the forward transform by:\n A. an inverse root and an `n^-1` scaling\n B. a larger modulus\n C. removing all twiddle factors\n\n3. The direct matrix transform is still pedagogically useful because:\n A. it keeps every coefficient contribution visible\n B. it is how Kyber is implemented directly at full size\n C. it removes the need for butterflies\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Answer Key" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"A\", 3: \"A\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Round-Trip Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Round-Trip Check\n\nVerify by code that `INTT\u03c8(NTT\u03c8(a)) = a` for a nontrivial vector.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Check The Round Trip" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Check The Round Trip\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi\n\nsignal = [6, 0, 5, 2]\npsi = find_psi(4, 17)\nrecovered = inverse_ntt_psi(forward_ntt_psi(signal, 17, psi), 17, psi)\nprint(recovered)\nassert recovered == signal\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Written Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nIn one paragraph, explain why the direct transform is the right place to understand the algebra, but not the right place to stop if you care about algorithmic speed.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional Challenge" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nfrom ntt_learning.toy_ntt import find_psi, forward_ntt_psi\n\npsi = find_psi(4, 17)\nfor signal in ([1, 1, 1, 1], [0, 1, 0, 1], [3, 5, 7, 9]):\n print(signal, \"->\", forward_ntt_psi(signal, 17, psi))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `studio.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Problems: Negative-Wrapped NTT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb b/notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb new file mode 100644 index 0000000..c3ce961 --- /dev/null +++ b/notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Studio Goals" + } + }, + "source": "## META | difficulty 1 | Studio Goals\n\nThe studio compares direct positive-wrapped and negative-wrapped transforms so the learner stops treating \u201cNTT\u201d as one unqualified object.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Same Input, Different Transform Story" + } + }, + "source": "## MANDATORY | difficulty 3 | Same Input, Different Transform Story\n\nThe same coefficient vector can be sent through two different transform stories depending on the quotient ring.\nThat difference is not implementation noise. It is structural.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Compare Positive-Wrapped And Negative-Wrapped Views" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Compare Positive-Wrapped And Negative-Wrapped Views\n\nfrom ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt, forward_ntt_psi\n\nsignal = [1, 2, 3, 4]\nmodulus = 17\nomega = find_primitive_root(4, modulus)\npsi = find_psi(4, modulus)\n\nprint(\"positive-wrapped NTT:\", forward_ntt(signal, modulus, omega))\nprint(\"negative-wrapped NTT_psi:\", forward_ntt_psi(signal, modulus, psi))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Debug Checklist" + } + }, + "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nIf a direct transform result looks suspicious, inspect:\n\n1. the chosen modulus\n2. the order of the root\n3. whether you are using `\u03c9` or `\u03c8`\n4. whether the inverse includes `n^-1`\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "See A Wrong-Root Failure" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | See A Wrong-Root Failure\n\nfrom ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt_psi\n\nsignal = [1, 2, 3, 4]\nmodulus = 17\nomega = find_primitive_root(4, modulus)\npsi = find_psi(4, modulus)\n\nprint(\"correct psi-based transform:\", forward_ntt_psi(signal, modulus, psi))\nprint(\"wrongly using omega as if it were psi:\", forward_ntt_psi(signal, modulus, omega))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nExplain why \u201cpick any root of unity\u201d is not an acceptable habit in this subject.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Matrix Comparison" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Matrix Comparison\n\nfrom ntt_learning.toy_ntt import find_primitive_root, find_psi, ntt_psi_matrix\n\nmodulus = 17\nomega = find_primitive_root(4, modulus)\npsi = find_psi(4, modulus)\n\nprint(\"omega:\", omega)\nprint(\"psi:\", psi)\nfor row in ntt_psi_matrix(4, modulus, psi):\n print(row)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../butterfly_mechanics/03_fast_forward_ct/lecture.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Studio: Negative-Wrapped NTT", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb new file mode 100644 index 0000000..483f535 --- /dev/null +++ b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Lab Goals" + } + }, + "source": "## META | difficulty 1 | Lab Goals\n\nThe lab is about making the Kyber modulus obstruction explicit enough that it becomes memorable.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 1" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 1\n\nBefore running the next cell, say out loud:\n\n- why `256 | 3328`\n- why `512` does not divide `3328`\n- what that means for the existence of `\u03c8`\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check\n\nprint(\"3328 / 256 =\", (3329 - 1) // 256)\nprint(\"3328 % 256 =\", (3329 - 1) % 256)\nprint(\"3328 % 512 =\", (3329 - 1) % 512)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 2" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 2\n\nVerify the toy base multiplication by hand before running the next cell.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Check The Toy Base Multiplication" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Check The Toy Base Multiplication\n\nfrom ntt_learning.toy_ntt import base_multiply_pair\n\nprint(base_multiply_pair([3, 5], [2, 7], zeta=6, modulus=17))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- Why is \u201cthere is no 512-th root\u201d not just a technical footnote?\n- What false picture of Kyber would survive if you ignored that fact?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Tiny Modulus Classifier" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Tiny Modulus Classifier\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\ndef classify(q=17, n=4):\n print({\"q\": q, \"n\": n, \"pwc\": (q - 1) % n == 0, \"nwc\": (q - 1) % (2 * n) == 0})\n\ndisplay(widgets.interact(classify, q=(5, 101), n=(2, 16)))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `problems.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lab: Kyber NTT And Base Multiplication", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb new file mode 100644 index 0000000..4b9dd45 --- /dev/null +++ b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb @@ -0,0 +1,177 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Objectives" + } + }, + "source": "## META | difficulty 1 | Objectives\n\nThis bundle ties the transform story to Kyber without throwing away the concrete arithmetic.\n\nFocus:\n\n- what Kyber\u2019s `q = 3329`, `n = 256` really allow\n- why the full `2n`-th root mental model breaks at Kyber v3\n- why base multiplication appears\n- how to keep the story NTT-centered without lying about the modulus\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Kyber Is Not \u201cJust Generic Negacyclic NTT With Big Numbers\u201d" + } + }, + "source": "## MANDATORY | difficulty 3 | Kyber Is Not \u201cJust Generic Negacyclic NTT With Big Numbers\u201d\n\nThe key arithmetic reality is:\n\n- Kyber v3 has `n = 256`\n- `q = 3329`\n- `256` divides `3328`\n- `512` does **not** divide `3328`\n\nThat means a primitive `256`-th root exists, but a primitive `512`-th root does not.\nSo the clean full-length `\u03c8` story from the toy negative-wrapped transform does not lift over unchanged.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Check The Kyber Root Reality Directly" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Check The Kyber Root Reality Directly\n\nfrom ntt_learning.toy_ntt import find_primitive_root\n\nprint(\"3329 - 1 =\", 3329 - 1)\nprint(\"primitive 256-th root in Z_3329:\", find_primitive_root(256, 3329))\ntry:\n find_primitive_root(512, 3329)\nexcept Exception as exc:\n print(\"512-th root fails exactly because:\", exc)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Tiny Analogue Of The Same Obstruction" + } + }, + "source": "## MANDATORY | difficulty 3 | Tiny Analogue Of The Same Obstruction\n\nThe easiest way to feel this is to shrink the numbers.\nIn `Z_13`, a 4-th root exists because `4 | 12`, but an 8-th root does not because `8` does not divide `12`.\n\nThat is the same shape of obstruction as Kyber v3, only tiny enough to inspect instantly.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Run The Tiny Analogue" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Run The Tiny Analogue\n\nfrom ntt_learning.toy_ntt import find_primitive_root\n\nprint(\"primitive 4-th root in Z_13:\", find_primitive_root(4, 13))\ntry:\n find_primitive_root(8, 13)\nexcept Exception as exc:\n print(\"8-th root fails:\", exc)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Why Base Multiplication Appears" + } + }, + "source": "## MANDATORY | difficulty 3 | Why Base Multiplication Appears\n\nOnce the ring does not split into fully scalar transform slots in the naive `\u03c8` way, multiplication in the transform domain is no longer \u201cjust multiply scalars slot by slot\u201d.\n\nA small block structure remains, and that is why pairwise base multiplication appears.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "See A Toy Base Multiplication" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | See A Toy Base Multiplication\n\nfrom ntt_learning.toy_ntt import base_multiply_pair\n\nleft = [7, 11]\nright = [5, 13]\nzeta = 4\nmodulus = 17\n\nraw = [left[0] * right[0], left[0] * right[1] + left[1] * right[0], left[1] * right[1]]\nreduced = [(raw[0] + zeta * raw[2]) % modulus, raw[1] % modulus]\n\nprint(\"raw degree-2 product:\", raw)\nprint(\"reduce with x^2 = zeta:\", reduced)\nprint(\"base_multiply_pair:\", base_multiply_pair(left, right, zeta, modulus))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Retrieval Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\n1. What exact divisibility fact blocks the naive full `2n`-th root story in Kyber v3?\n2. Why does that obstruction point you toward base multiplication?\n3. Why would it be misleading to teach Kyber as if the toy `\u03c8` story carried over unchanged?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Compare Several Moduli" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Compare Several Moduli\n\ndef status(n, q):\n pwc = (q - 1) % n == 0\n nwc = (q - 1) % (2 * n) == 0\n return {\"n\": n, \"q\": q, \"pwc\": pwc, \"nwc\": nwc}\n\nfor sample in [(4, 17), (4, 13), (256, 7681), (256, 3329)]:\n print(status(*sample))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `lab.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lecture: Kyber NTT And Base Multiplication", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb new file mode 100644 index 0000000..5c90b30 --- /dev/null +++ b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Problem Set Goals" + } + }, + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the Kyber-specific modulus story is now precise instead of fuzzy.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Multiple-Choice Retrieval" + } + }, + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. For Kyber v3, the important root fact is:\n A. `512` divides `3328`\n B. `256` divides `3328` but `512` does not\n C. no relevant root exists at all\n\n2. Base multiplication appears because:\n A. the transform-domain multiplication remains structured in small blocks\n B. scalar multiplication is forbidden in finite fields\n C. schoolbook multiplication vanished\n\n3. The biggest pedagogical risk is:\n A. teaching Kyber as if the toy `\u03c8` model applied unchanged\n B. teaching any toy examples at all\n C. comparing moduli\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Answer Key" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"A\", 3: \"A\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Quick Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Quick Check\n\nExplain in one sentence why a primitive 256-th root is not enough to recover the full toy `\u03c8` story at `n = 256`.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Numerical Check" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Numerical Check\n\nprint(\"2 * 256 =\", 2 * 256)\nprint(\"3329 - 1 =\", 3329 - 1)\nprint(\"divides?\", (3329 - 1) % (2 * 256) == 0)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Written Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nIn one paragraph, explain why the Kyber notebook belongs after the toy direct and fast-transform notebooks rather than before them.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional Challenge" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nsamples = [(256, 7681), (256, 3329), (8, 97), (8, 41)]\nfor n, q in samples:\n print({\"n\": n, \"q\": q, \"n_divides_q_minus_1\": (q - 1) % n == 0, \"two_n_divides_q_minus_1\": (q - 1) % (2 * n) == 0})\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `studio.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Problems: Kyber NTT And Base Multiplication", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb new file mode 100644 index 0000000..31ecaf0 --- /dev/null +++ b/notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Studio Goals" + } + }, + "source": "## META | difficulty 1 | Studio Goals\n\nThis studio compares the clean toy story with the Kyber-specific modulus reality and treats the mismatch as the lesson.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "The Mismatch Is Not A Bug In The Course" + } + }, + "source": "## MANDATORY | difficulty 3 | The Mismatch Is Not A Bug In The Course\n\nThe mismatch between \u201ctoy full `\u03c8` story\u201d and \u201cKyber v3 modulus reality\u201d is exactly what the learner needs to understand.\n\nThat mismatch is why base multiplication and implementation-specific scheduling matter.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Toy Full \u03c8 Story vs Kyber Root Reality" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Toy Full \u03c8 Story vs Kyber Root Reality\n\nfrom ntt_learning.toy_ntt import find_psi\n\nprint(\"toy n=4, q=17 has psi:\", find_psi(4, 17))\ntry:\n find_psi(256, 3329)\nexcept Exception as exc:\n print(\"Kyber v3 does not have that full psi story:\", exc)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Debug Checklist" + } + }, + "source": "## MANDATORY | difficulty 2 | Debug Checklist\n\nIf somebody says \u201cKyber is just the same toy negative-wrapped NTT with bigger numbers\u201d, inspect:\n\n1. whether they checked `2n | q - 1`\n2. whether they accounted for the missing `\u03c8`\n3. whether they know why base multiplication appears\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "See The Exact Obstruction Again" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | See The Exact Obstruction Again\n\nq = 3329\nn = 256\nprint({\"q_minus_1\": q - 1, \"n\": n, \"2n\": 2 * n, \"q_minus_1_mod_n\": (q - 1) % n, \"q_minus_1_mod_2n\": (q - 1) % (2 * n)})\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nExplain why the Kyber base-multiplication story is easier to trust once you have already internalized the toy negacyclic transform.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Another toy base multiplication" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Another toy base multiplication\n\nfrom ntt_learning.toy_ntt import base_multiply_pair\n\nprint(base_multiply_pair([7, 9], [4, 6], zeta=11, modulus=17))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../professional/06_debugging_ntt_failures/lecture.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Studio: Kyber NTT And Base Multiplication", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/professional/06_debugging_ntt_failures/lab.ipynb b/notebooks/professional/06_debugging_ntt_failures/lab.ipynb new file mode 100644 index 0000000..bb91cc0 --- /dev/null +++ b/notebooks/professional/06_debugging_ntt_failures/lab.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Lab Goals" + } + }, + "source": "## META | difficulty 1 | Lab Goals\n\nThe lab asks you to match output fingerprints to the underlying bug.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 1" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 1\n\nBefore running the next cell, predict which of these bug labels goes with each fingerprint:\n\n- shuffled but otherwise familiar values\n- values that look uniformly too large\n- values broken already in a local stage pair\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Prediction Check" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Prediction Check\n\nfingerprints = {\n \"wrong_order\": \"shuffled but same value set\",\n \"missing_scale\": \"same shape but uniformly off by a factor\",\n \"wrong_zeta\": \"local pair outputs go bad immediately\",\n}\nprint(fingerprints)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "Exercise 2" + } + }, + "source": "## MANDATORY | difficulty 3 | Exercise 2\n\nExplain why \u201calmost right\u201d is a useless debugging description unless you also specify whether the issue is:\n\n- sign\n- order\n- zeta\n- scaling\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "exercise", + "title": "A Small Debugging Drill" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | A Small Debugging Drill\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace\n\nforward_trace = fast_ntt_psi_ct_trace([5, 6, 7, 8], 7681, 1925)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925)\n\nprint(\"forward BO output:\", forward_trace.raw_output)\nprint(\"forward NO output:\", forward_trace.normal_order_output)\nprint(\"inverse unscaled:\", inverse_trace.raw_output)\nprint(\"inverse scaled:\", inverse_trace.scaled_output)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Reflection\n\nReflection prompt:\n\n- Which bug fingerprint feels easiest to recognize now?\n- Which one still needs more repetition?\n- What is your debugging order of operations now?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Build Your Own Fingerprint Table" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Build Your Own Fingerprint Table\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\ndef note_bug(mode=\"wrong_order\"):\n print({\"mode\": mode, \"what_to_check_first\": {\"wrong_order\": \"bit reversal\", \"missing_scale\": \"n^-1\", \"wrong_zeta\": \"local pair twiddle\", \"wrong_sign\": \"negacyclic fold sign\"}[mode]})\n\ndisplay(widgets.interact(note_bug, mode=[\"wrong_order\", \"missing_scale\", \"wrong_zeta\", \"wrong_sign\"]))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `problems.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lab: Debugging NTT Failures", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/professional/06_debugging_ntt_failures/lecture.ipynb b/notebooks/professional/06_debugging_ntt_failures/lecture.ipynb new file mode 100644 index 0000000..1de5322 --- /dev/null +++ b/notebooks/professional/06_debugging_ntt_failures/lecture.ipynb @@ -0,0 +1,139 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Objectives" + } + }, + "source": "## META | difficulty 1 | Objectives\n\nThis final bundle turns common failure modes into visible patterns instead of vague warnings.\n\nFocus:\n\n- wrong sign in wraparound\n- wrong root or wrong zeta\n- wrong BO / NO comparison\n- missing final scaling\n- wrong mental model for the Kyber modulus\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "Bad Outputs Have Fingerprints" + } + }, + "source": "## MANDATORY | difficulty 3 | Bad Outputs Have Fingerprints\n\nDebugging NTTs is easier when you stop staring at the final vector as one blob.\nEach common mistake leaves a characteristic fingerprint:\n\n- wrong sign flips specific wrapped slots\n- wrong order makes a correct value set appear shuffled\n- missing `n^-1` keeps the shape but scales everything wrong\n- wrong zeta corrupts local pair structure early\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "See Four Failure Modes Side By Side" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | See Four Failure Modes Side By Side\n\nfrom ntt_learning.toy_ntt import (\n fast_intt_psi_gs_trace,\n fast_ntt_psi_ct_trace,\n forward_ntt_psi,\n negacyclic_reduce,\n schoolbook_convolution,\n)\n\nsignal = [1, 2, 3, 4]\nforward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925)\n\nraw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8])\nwrong_sign = [raw[0] + raw[4], raw[1] + raw[5], raw[2] + raw[6], raw[3]]\nwrong_order = list(forward_trace.raw_output)\nwrong_scale = list(inverse_trace.raw_output)\nwrong_root = forward_ntt_psi(signal, 7681, 3383)\n\nprint(\"wrong sign fold:\", wrong_sign)\nprint(\"correct sign fold:\", negacyclic_reduce(raw, n=4))\nprint(\"wrong BO-vs-NO comparison:\", wrong_order)\nprint(\"correct NO output:\", forward_trace.normal_order_output)\nprint(\"missing final scaling:\", wrong_scale)\nprint(\"wrong root in direct transform:\", wrong_root)\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Interactive Failure Picker" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Interactive Failure Picker\n\nimport ipywidgets as widgets\nfrom IPython.display import display\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace, forward_ntt_psi\n\nforward_trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925)\n\nfailures = {\n \"wrong_order\": list(forward_trace.raw_output),\n \"correct_order\": list(forward_trace.normal_order_output),\n \"missing_scale\": list(inverse_trace.raw_output),\n \"scaled\": list(inverse_trace.scaled_output),\n \"wrong_root\": forward_ntt_psi([1, 2, 3, 4], 7681, 3383),\n}\n\ndef preview(mode=\"wrong_order\"):\n print(mode, \"->\", failures[mode])\n\ndisplay(widgets.interact(preview, mode=sorted(failures)))\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Retrieval Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Retrieval Check\n\n1. Which mistake keeps the general shape of the inverse output but leaves every entry too large by a shared factor?\n2. Which mistake often disappears once you apply the correct BO -> NO reorder?\n3. Which mistake shows up earliest in local pair traces rather than only at the very end?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Trace Rows For Debugging" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Trace Rows For Debugging\n\nfrom ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, stage_rows\n\ntrace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925)\nfor stage in trace.stages:\n print(\"stage\", stage.stage_index)\n for row in stage_rows(stage):\n print(row)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `lab.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Lecture: Debugging NTT Failures", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/professional/06_debugging_ntt_failures/problems.ipynb b/notebooks/professional/06_debugging_ntt_failures/problems.ipynb new file mode 100644 index 0000000..201b5f1 --- /dev/null +++ b/notebooks/professional/06_debugging_ntt_failures/problems.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Problem Set Goals" + } + }, + "source": "## META | difficulty 1 | Problem Set Goals\n\nThis notebook checks whether the main NTT bug classes are now distinct in memory.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Multiple-Choice Retrieval" + } + }, + "source": "## MANDATORY | difficulty 2 | Multiple-Choice Retrieval\n\nChoose one answer for each:\n\n1. A shuffled but otherwise familiar forward result most strongly suggests:\n A. missing final scaling\n B. wrong BO / NO comparison\n C. wrong modulus\n\n2. An inverse output that looks like a clean multiple of the target most strongly suggests:\n A. missing `n^-1`\n B. wrong wraparound sign\n C. wrong bit-reversal map\n\n3. A local pair that already looks broken in stage 1 most strongly suggests:\n A. wrong zeta\n B. correct CT output\n C. harmless ordering noise\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "quiz", + "title": "Answer Key" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | Answer Key\n\nanswers = {1: \"B\", 2: \"A\", 3: \"A\"}\nprint(answers)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Debug Priority Check" + } + }, + "source": "## MANDATORY | difficulty 2 | Debug Priority Check\n\nList the first four checks you would run on a suspicious iNTT output.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "One Good Ordering" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | One Good Ordering\n\ndebug_order = [\n \"check BO / NO assumption\",\n \"check root / zeta schedule\",\n \"check final n^-1 scaling\",\n \"check sign / wraparound conventions\",\n]\nprint(debug_order)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Written Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Written Reflection\n\nIn one paragraph, explain why visible traces are much better debugging tools than only comparing final vectors.\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional Challenge" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional Challenge\n\nmistakes = {\n \"wrong_order\": \"fix the permutation first\",\n \"missing_scale\": \"multiply by n^-1\",\n \"wrong_zeta\": \"rebuild the twiddle schedule\",\n \"wrong_sign\": \"inspect the quotient ring\",\n}\nfor key, value in mistakes.items():\n print(key, \"->\", value)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `studio.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Problems: Debugging NTT Failures", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/professional/06_debugging_ntt_failures/studio.ipynb b/notebooks/professional/06_debugging_ntt_failures/studio.ipynb new file mode 100644 index 0000000..e82fe79 --- /dev/null +++ b/notebooks/professional/06_debugging_ntt_failures/studio.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "orientation", + "title": "Studio Goals" + } + }, + "source": "## META | difficulty 1 | Studio Goals\n\nThe last studio compresses the whole course into one debugging mindset.\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "explanation", + "title": "The Whole Course Is A Debugging Ladder" + } + }, + "source": "## MANDATORY | difficulty 3 | The Whole Course Is A Debugging Ladder\n\nEvery earlier bundle built one layer of the debugging stack:\n\n- schoolbook grid and wraparound\n- direct transform algebra\n- CT stage schedule\n- GS inverse schedule\n- bit-reversal and scaling\n- Kyber modulus constraints and base multiplication\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 3, + "kind": "demo", + "title": "Print The Whole Debugging Ladder" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 3 | Print The Whole Debugging Ladder\n\nladder = [\n \"Can I see the raw schoolbook product grid?\",\n \"Can I explain the negacyclic wraparound sign?\",\n \"Can I reproduce the direct NTT_psi / INTT_psi round trip?\",\n \"Can I trace CT stages with the right zetas?\",\n \"Can I trace GS stages with the right order and scaling?\",\n \"Can I explain why Kyber v3 does not inherit the full toy psi story unchanged?\",\n]\nfor item in ladder:\n print(\"-\", item)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "Final Debug Checklist" + } + }, + "source": "## MANDATORY | difficulty 2 | Final Debug Checklist\n\nKeep this order:\n\n1. ring rule and wraparound\n2. root existence and root choice\n3. stage pairings and zetas\n4. BO vs NO comparison\n5. final `n^-1` scaling\n6. Kyber-specific modulus / base-multiplication assumptions\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "exercise", + "title": "One Last Round Trip" + } + }, + "outputs": [], + "source": "# MANDATORY | difficulty 2 | One Last Round Trip\n\nfrom ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace\n\nsignal = [3, 1, 4, 1]\nforward_trace = fast_ntt_psi_ct_trace(signal, 17, 2)\ninverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 17, 2)\n\nprint(\"signal:\", signal)\nprint(\"forward BO:\", forward_trace.raw_output)\nprint(\"inverse scaled:\", inverse_trace.scaled_output)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "mandatory", + "difficulty": 2, + "kind": "reflection", + "title": "Final Reflection" + } + }, + "source": "## MANDATORY | difficulty 2 | Final Reflection\n\nFinal prompt:\n\n- Which image now anchors your understanding of NTT best: the schoolbook grid, the fold arrows, the CT stage view, the GS stage view, or the bit-reversal map?\n- Why that one?\n" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "pedagogy": { + "role": "facultative", + "difficulty": 4, + "kind": "exploration", + "title": "Optional: Personal Debug Rule" + } + }, + "outputs": [], + "source": "# FACULTATIVE | difficulty 4 | Optional: Personal Debug Rule\n\npersonal_rule = \"Never trust a final vector until I have checked the stage trace, the order, and the scaling.\"\nprint(personal_rule)\n" + }, + { + "cell_type": "markdown", + "metadata": { + "pedagogy": { + "role": "meta", + "difficulty": 1, + "kind": "handoff", + "title": "Next Notebook" + } + }, + "source": "## META | difficulty 1 | Next Notebook\n\nNext notebook: `../../../COURSE_COMPLETE.ipynb`\n" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "ntt_learning": { + "title": "Studio: Debugging NTT Failures", + "contract_version": "0.2", + "sequence": [ + "notebooks/START_HERE.ipynb", + "notebooks/COURSE_BLUEPRINT.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", + "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lecture.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/lab.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/problems.ipynb", + "notebooks/foundations/02_negative_wrapped_ntt/studio.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lecture.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/lab.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/problems.ipynb", + "notebooks/butterfly_mechanics/03_fast_forward_ct/studio.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/lab.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/problems.ipynb", + "notebooks/butterfly_mechanics/04_fast_inverse_gs/studio.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/lab.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/problems.ipynb", + "notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication/studio.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lecture.ipynb", + "notebooks/professional/06_debugging_ntt_failures/lab.ipynb", + "notebooks/professional/06_debugging_ntt_failures/problems.ipynb", + "notebooks/professional/06_debugging_ntt_failures/studio.ipynb", + "notebooks/COURSE_COMPLETE.ipynb" + ] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ntt_learning/__init__.py b/ntt_learning/__init__.py index 2435337..61a731b 100644 --- a/ntt_learning/__init__.py +++ b/ntt_learning/__init__.py @@ -3,4 +3,3 @@ from .course import ALL_NOTEBOOKS, ROUTE_NOTEBOOKS, TECHNICAL_NOTEBOOKS __all__ = ["ALL_NOTEBOOKS", "ROUTE_NOTEBOOKS", "TECHNICAL_NOTEBOOKS"] - diff --git a/ntt_learning/course.py b/ntt_learning/course.py index cd6204c..918d26c 100644 --- a/ntt_learning/course.py +++ b/ntt_learning/course.py @@ -9,23 +9,37 @@ REPO_ROOT = Path(__file__).resolve().parent.parent ROUTE_NOTEBOOKS = [ Path("notebooks/START_HERE.ipynb"), Path("notebooks/COURSE_BLUEPRINT.ipynb"), + Path("notebooks/COURSE_COMPLETE.ipynb"), ] -FOUNDATION_BUNDLE_DIR = Path("notebooks/foundations/01_convolution_to_toy_ntt") - -TECHNICAL_NOTEBOOKS = [ - FOUNDATION_BUNDLE_DIR / "lecture.ipynb", - FOUNDATION_BUNDLE_DIR / "lab.ipynb", - FOUNDATION_BUNDLE_DIR / "problems.ipynb", - FOUNDATION_BUNDLE_DIR / "studio.ipynb", +BUNDLE_DIRS = [ + Path("notebooks/foundations/01_convolution_to_toy_ntt"), + Path("notebooks/foundations/02_negative_wrapped_ntt"), + Path("notebooks/butterfly_mechanics/03_fast_forward_ct"), + Path("notebooks/butterfly_mechanics/04_fast_inverse_gs"), + Path("notebooks/kyber_mapping/05_kyber_ntt_and_base_multiplication"), + Path("notebooks/professional/06_debugging_ntt_failures"), ] -ALL_NOTEBOOKS = ROUTE_NOTEBOOKS + TECHNICAL_NOTEBOOKS + +def bundle_notebooks(bundle_dir: Path) -> list[Path]: + return [ + bundle_dir / "lecture.ipynb", + bundle_dir / "lab.ipynb", + bundle_dir / "problems.ipynb", + bundle_dir / "studio.ipynb", + ] + + +TECHNICAL_NOTEBOOKS = [notebook for bundle_dir in BUNDLE_DIRS for notebook in bundle_notebooks(bundle_dir)] + +ALL_NOTEBOOKS = ROUTE_NOTEBOOKS[:2] + TECHNICAL_NOTEBOOKS + ROUTE_NOTEBOOKS[2:] NOTEBOOK_SEQUENCE = [ ROUTE_NOTEBOOKS[0], ROUTE_NOTEBOOKS[1], *TECHNICAL_NOTEBOOKS, + ROUTE_NOTEBOOKS[2], ] REQUIRED_SCRIPT_NAMES = [ @@ -42,4 +56,3 @@ CELL_ROLES = {"meta", "mandatory", "facultative"} ROUTE_ONLY_ROLES = {"meta", "mandatory"} MANDATORY_DIFFICULTIES = {1, 2, 3} FACULTATIVE_DIFFICULTIES = {4, 5, 6, 7, 8, 9, 10} - diff --git a/ntt_learning/toy_ntt.py b/ntt_learning/toy_ntt.py index 22a42d2..bf994c9 100644 --- a/ntt_learning/toy_ntt.py +++ b/ntt_learning/toy_ntt.py @@ -1,4 +1,4 @@ -"""Small, inspectable helpers for the first NTT learning module.""" +"""Small, inspectable helpers for the NTT learning course.""" from __future__ import annotations @@ -26,6 +26,35 @@ def schoolbook_convolution( return _apply_mod(result, modulus) +def pairwise_product_grid(left: Sequence[int], right: Sequence[int]) -> list[list[int]]: + """Return the full schoolbook multiplication grid.""" + return [[int(left_value * right_value) for right_value in right] for left_value in left] + + +def convolution_contributions(left: Sequence[int], right: Sequence[int]) -> list[dict[str, object]]: + """Return each output index and the contributing schoolbook products.""" + raw = schoolbook_convolution(left, right) + rows: list[dict[str, object]] = [] + for output_index, total in enumerate(raw): + terms = [] + for left_index, left_value in enumerate(left): + right_index = output_index - left_index + if right_index < 0 or right_index >= len(right): + continue + right_value = right[right_index] + terms.append( + { + "left_index": left_index, + "right_index": right_index, + "left_value": int(left_value), + "right_value": int(right_value), + "product": int(left_value * right_value), + } + ) + rows.append({"output_index": output_index, "terms": terms, "total": int(total)}) + return rows + + def negacyclic_reduce( coefficients: Sequence[int], n: int, modulus: int | None = None ) -> list[int]: @@ -40,6 +69,36 @@ def negacyclic_reduce( return _apply_mod(reduced, modulus) +def wraparound_contributions( + coefficients: Sequence[int], n: int, negacyclic: bool = True +) -> list[dict[str, object]]: + """Describe how high-degree terms fold back into degree < n.""" + if n <= 0: + raise ValueError("n must be positive") + + reduced = [0] * n + rows: list[list[dict[str, int]]] = [[] for _ in range(n)] + for index, coefficient in enumerate(coefficients): + wraps, slot = divmod(index, n) + sign = -1 if negacyclic and wraps % 2 else 1 + signed_value = int(sign * coefficient) + reduced[slot] += signed_value + rows[slot].append( + { + "source_index": index, + "wraps": wraps, + "sign": sign, + "value": int(coefficient), + "signed_value": signed_value, + } + ) + + return [ + {"slot": slot, "contributions": rows[slot], "total": int(reduced[slot])} + for slot in range(n) + ] + + def negacyclic_multiply( left: Sequence[int], right: Sequence[int], n: int, modulus: int | None = None ) -> list[int]: @@ -55,6 +114,36 @@ def mod_inverse(value: int, modulus: int) -> int: return pow(value, -1, modulus) +def pointwise_multiply( + left: Sequence[int], right: Sequence[int], modulus: int | None = None +) -> list[int]: + """Multiply transform-domain entries slot by slot.""" + if len(left) != len(right): + raise ValueError("left and right must have the same length") + products = [int(a * b) for a, b in zip(left, right)] + return _apply_mod(products, modulus) + + +def scale_values(values: Sequence[int], factor: int, modulus: int | None = None) -> list[int]: + """Multiply all values by a scalar.""" + scaled = [int(factor * value) for value in values] + return _apply_mod(scaled, modulus) + + +def base_multiply_pair( + left: Sequence[int], right: Sequence[int], zeta: int, modulus: int +) -> list[int]: + """Multiply two degree-1 polynomials in a quadratic factor ring.""" + if len(left) != 2 or len(right) != 2: + raise ValueError("base_multiply_pair expects two 2-term coefficient vectors") + a0, a1 = (int(left[0]), int(left[1])) + b0, b1 = (int(right[0]), int(right[1])) + return [ + (a0 * b0 + zeta * a1 * b1) % modulus, + (a0 * b1 + a1 * b0) % modulus, + ] + + def _proper_divisors(order: int) -> list[int]: return [candidate for candidate in range(1, order) if order % candidate == 0] @@ -76,8 +165,17 @@ def find_primitive_root(order: int, modulus: int) -> int: raise ValueError(f"no primitive {order}-th root exists modulo {modulus}") +def find_psi(order: int, modulus: int) -> int: + """Find a primitive ``2 * order``-th root for negative-wrapped NTT.""" + full_order = 2 * order + root = find_primitive_root(full_order, modulus) + if pow(root, order, modulus) != (modulus - 1) % modulus: + raise ValueError(f"primitive {full_order}-th root does not satisfy psi^order = -1") + return root + + def forward_ntt(values: Sequence[int], modulus: int, omega: int) -> list[int]: - """Definition-first NTT using the matrix viewpoint.""" + """Definition-first positive-wrapped NTT using the matrix viewpoint.""" n = len(values) if n == 0: return [] @@ -108,6 +206,50 @@ def inverse_ntt(values: Sequence[int], modulus: int, omega: int) -> list[int]: return recovered +def ntt_psi_exponent_grid(length: int) -> list[list[int]]: + """Return the exponent grid used by direct negative-wrapped NTT.""" + return [[2 * row * column + row for row in range(length)] for column in range(length)] + + +def ntt_psi_matrix(length: int, modulus: int, psi: int) -> list[list[int]]: + """Return the direct negative-wrapped transform matrix.""" + return [ + [pow(psi, 2 * row * column + row, modulus) for row in range(length)] + for column in range(length) + ] + + +def forward_ntt_psi(values: Sequence[int], modulus: int, psi: int) -> list[int]: + """Definition-first negative-wrapped NTT using a 2n-th root ``psi``.""" + n = len(values) + if n == 0: + return [] + + spectrum = [] + for column in range(n): + total = 0 + for row, value in enumerate(values): + total += value * pow(psi, 2 * row * column + row, modulus) + spectrum.append(total % modulus) + return spectrum + + +def inverse_ntt_psi(values: Sequence[int], modulus: int, psi: int) -> list[int]: + """Inverse of ``forward_ntt_psi``.""" + n = len(values) + if n == 0: + return [] + + n_inverse = mod_inverse(n, modulus) + recovered = [] + for row in range(n): + total = 0 + for column, value in enumerate(values): + total += value * pow(psi, -(2 * row * column + row), modulus) + recovered.append((n_inverse * total) % modulus) + return recovered + + def ct_butterfly_pair(top: int, bottom: int, zeta: int, modulus: int) -> tuple[int, int]: """One Cooley-Tukey style butterfly on a pair.""" twiddled = (zeta * bottom) % modulus @@ -156,6 +298,33 @@ class ButterflyAction: outputs: tuple[int, int] +@dataclass(frozen=True) +class TransformStage: + """One visualizable stage in a fast NTT / iNTT trace.""" + + algorithm: str + stage_index: int + input_values: tuple[int, ...] + output_values: tuple[int, ...] + pairings: tuple[tuple[int, int], ...] + zetas: tuple[int, ...] + note: str + + +@dataclass(frozen=True) +class TransformTrace: + """A full fast-transform trace suitable for notebook visualization.""" + + algorithm: str + modulus: int + root: int + input_values: tuple[int, ...] + stages: tuple[TransformStage, ...] + raw_output: tuple[int, ...] + normal_order_output: tuple[int, ...] + scaled_output: tuple[int, ...] | None = None + + def apply_ct_stage( values: Sequence[int], block_size: int, zetas: int | Iterable[int], modulus: int ) -> tuple[list[int], list[ButterflyAction]]: @@ -217,6 +386,22 @@ def action_rows(actions: Sequence[ButterflyAction]) -> list[dict[str, object]]: ] +def stage_rows(stage: TransformStage) -> list[dict[str, object]]: + """Return rows describing the pair operations of one trace stage.""" + rows = [] + for pair, zeta in zip(stage.pairings, stage.zetas): + left, right = pair + rows.append( + { + "pair": pair, + "zeta": zeta, + "inputs": (stage.input_values[left], stage.input_values[right]), + "outputs": (stage.output_values[left], stage.output_values[right]), + } + ) + return rows + + def bit_reverse(index: int, width: int) -> int: """Reverse ``width`` bits from ``index``.""" if width < 0: @@ -229,14 +414,202 @@ def bit_reverse(index: int, width: int) -> int: return reversed_bits +def bit_reversed_indices(length: int) -> list[int]: + """Return the bit-reversal permutation for ``length``.""" + if length <= 0: + raise ValueError("length must be positive") + if length & (length - 1): + raise ValueError("bit_reversed_indices requires a power-of-two length") + + width = length.bit_length() - 1 + return [bit_reverse(index, width) for index in range(length)] + + def bit_reversed_order(values: Sequence[int]) -> list[int]: """Return the array reordered by bit-reversed indices.""" length = len(values) if length == 0: return [] - if length & (length - 1): - raise ValueError("bit_reversed_order requires a power-of-two length") + permutation = bit_reversed_indices(length) + return [values[index] for index in permutation] - width = length.bit_length() - 1 - return [values[bit_reverse(index, width)] for index in range(length)] +def _interleave(left: Sequence[int], right: Sequence[int]) -> list[int]: + result: list[int] = [] + for left_value, right_value in zip(left, right): + result.extend([int(left_value), int(right_value)]) + return result + + +def _renumber_stages(stages: Sequence[TransformStage]) -> tuple[TransformStage, ...]: + return tuple( + TransformStage( + algorithm=stage.algorithm, + stage_index=index + 1, + input_values=stage.input_values, + output_values=stage.output_values, + pairings=stage.pairings, + zetas=stage.zetas, + note=stage.note, + ) + for index, stage in enumerate(stages) + ) + + +def _merge_child_stages( + left_stages: Sequence[TransformStage], right_stages: Sequence[TransformStage] +) -> list[TransformStage]: + if len(left_stages) != len(right_stages): + raise ValueError("expected the same number of stages on both recursive branches") + + merged: list[TransformStage] = [] + for left_stage, right_stage in zip(left_stages, right_stages): + merged.append( + TransformStage( + algorithm=left_stage.algorithm, + stage_index=0, + input_values=tuple(_interleave(left_stage.input_values, right_stage.input_values)), + output_values=tuple(_interleave(left_stage.output_values, right_stage.output_values)), + pairings=tuple((2 * a, 2 * b) for a, b in left_stage.pairings) + + tuple((2 * a + 1, 2 * b + 1) for a, b in right_stage.pairings), + zetas=left_stage.zetas + right_stage.zetas, + note=left_stage.note, + ) + ) + return merged + + +def _fast_ntt_psi_ct_recursive( + values: Sequence[int], modulus: int, psi: int +) -> tuple[tuple[int, ...], list[TransformStage]]: + size = len(values) + if size == 1: + return (int(values[0]),), [] + + even_output, even_stages = _fast_ntt_psi_ct_recursive(values[0::2], modulus, pow(psi, 2, modulus)) + odd_output, odd_stages = _fast_ntt_psi_ct_recursive(values[1::2], modulus, pow(psi, 2, modulus)) + + merged_stages = _merge_child_stages(even_stages, odd_stages) + stage_input = tuple(_interleave(even_output, odd_output)) + + output = list(stage_input) + pairings = [] + zetas = [] + for column in range(size // 2): + left = 2 * column + right = 2 * column + 1 + zeta = pow(psi, 2 * column + 1, modulus) + output[left], output[right] = ct_butterfly_pair(stage_input[left], stage_input[right], zeta, modulus) + pairings.append((left, right)) + zetas.append(zeta) + + merged_stages.append( + TransformStage( + algorithm="ct", + stage_index=0, + input_values=stage_input, + output_values=tuple(output), + pairings=tuple(pairings), + zetas=tuple(zetas), + note="Interleave recursive sub-transforms, then apply adjacent CT butterflies.", + ) + ) + return tuple(output), merged_stages + + +def fast_ntt_psi_ct_trace(values: Sequence[int], modulus: int, psi: int) -> TransformTrace: + """Return a stage-by-stage CT fast-NTT trace with BO output and NO comparison.""" + if not values: + return TransformTrace("ct", modulus, psi, (), (), (), (), None) + if len(values) & (len(values) - 1): + raise ValueError("fast_ntt_psi_ct_trace requires a power-of-two length") + + raw_output, stages = _fast_ntt_psi_ct_recursive(values, modulus, psi) + return TransformTrace( + algorithm="ct", + modulus=modulus, + root=psi, + input_values=tuple(int(value) for value in values), + stages=_renumber_stages(stages), + raw_output=raw_output, + normal_order_output=tuple(bit_reversed_order(raw_output)), + scaled_output=None, + ) + + +def fast_ntt_psi_ct(values: Sequence[int], modulus: int, psi: int) -> list[int]: + """Compute the BO output of the CT fast negative-wrapped NTT.""" + return list(fast_ntt_psi_ct_trace(values, modulus, psi).raw_output) + + +def _fast_intt_psi_gs_recursive( + values: Sequence[int], modulus: int, psi: int +) -> tuple[tuple[int, ...], list[TransformStage]]: + size = len(values) + if size == 1: + return (int(values[0]),), [] + + stage_input = tuple(int(value) for value in values) + stage_output = list(stage_input) + pairings = [] + zetas = [] + for column in range(size // 2): + left = 2 * column + right = 2 * column + 1 + zeta = mod_inverse(pow(psi, 2 * column + 1, modulus), modulus) + stage_output[left], stage_output[right] = gs_butterfly_pair( + stage_input[left], stage_input[right], zeta, modulus + ) + pairings.append((left, right)) + zetas.append(zeta) + + even_output, even_stages = _fast_intt_psi_gs_recursive( + stage_output[0::2], modulus, pow(psi, 2, modulus) + ) + odd_output, odd_stages = _fast_intt_psi_gs_recursive( + stage_output[1::2], modulus, pow(psi, 2, modulus) + ) + + return ( + tuple(_interleave(even_output, odd_output)), + [ + TransformStage( + algorithm="gs", + stage_index=0, + input_values=stage_input, + output_values=tuple(stage_output), + pairings=tuple(pairings), + zetas=tuple(zetas), + note="Apply adjacent GS butterflies, then recurse on even and odd branches.", + ), + *_merge_child_stages(even_stages, odd_stages), + ], + ) + + +def fast_intt_psi_gs_trace(values: Sequence[int], modulus: int, psi: int) -> TransformTrace: + """Return a stage-by-stage GS fast-iNTT trace from BO input to NO output.""" + if not values: + return TransformTrace("gs", modulus, psi, (), (), (), (), ()) + if len(values) & (len(values) - 1): + raise ValueError("fast_intt_psi_gs_trace requires a power-of-two length") + + raw_output, stages = _fast_intt_psi_gs_recursive(values, modulus, psi) + n_inverse = mod_inverse(len(values), modulus) + scaled_output = tuple((n_inverse * value) % modulus for value in raw_output) + return TransformTrace( + algorithm="gs", + modulus=modulus, + root=psi, + input_values=tuple(int(value) for value in values), + stages=_renumber_stages(stages), + raw_output=raw_output, + normal_order_output=raw_output, + scaled_output=scaled_output, + ) + + +def fast_intt_psi_gs(values: Sequence[int], modulus: int, psi: int) -> list[int]: + """Compute the NO output of the GS fast negative-wrapped inverse NTT.""" + trace = fast_intt_psi_gs_trace(values, modulus, psi) + return list(trace.scaled_output or ()) diff --git a/ntt_learning/visuals.py b/ntt_learning/visuals.py new file mode 100644 index 0000000..b7aea71 --- /dev/null +++ b/ntt_learning/visuals.py @@ -0,0 +1,329 @@ +"""Blunt visual helpers for the NTT notebooks.""" + +from __future__ import annotations + +from typing import Sequence + +import ipywidgets as widgets +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from IPython.display import clear_output, display + +from .toy_ntt import TransformStage, TransformTrace, pairwise_product_grid, wraparound_contributions + + +def _value_colors(values: Sequence[int]) -> list[str]: + colors = [] + for value in values: + if value < 0: + colors.append("#f08a5d") + elif value == 0: + colors.append("#d9d9d9") + else: + colors.append("#7ad3a8") + return colors + + +def _draw_value_row(ax, values: Sequence[int], y: float, prefix: str) -> None: + colors = _value_colors(values) + for index, (value, color) in enumerate(zip(values, colors)): + ax.text( + index, + y, + f"{prefix}{index}\n{value}", + ha="center", + va="center", + fontsize=10, + family="monospace", + bbox={ + "boxstyle": "round,pad=0.35", + "facecolor": color, + "edgecolor": "#222222", + "linewidth": 1.2, + }, + ) + + +def plot_convolution_grid( + left: Sequence[int], right: Sequence[int], title: str = "Schoolbook Product Grid" +): + """Plot the full schoolbook multiplication table and the diagonal sums.""" + grid = pairwise_product_grid(left, right) + diagonal_sums = [] + for diagonal in range(len(left) + len(right) - 1): + total = 0 + for row in range(len(left)): + column = diagonal - row + if 0 <= column < len(right): + total += grid[row][column] + diagonal_sums.append(total) + + fig, axes = plt.subplots(2, 1, figsize=(max(7, len(right) * 1.2), 6), height_ratios=[3, 1]) + heatmap_ax, sum_ax = axes + + heatmap_ax.imshow(grid, cmap="YlGnBu", aspect="auto") + heatmap_ax.set_title(title, fontsize=14, fontweight="bold") + heatmap_ax.set_xlabel("right coefficient index") + heatmap_ax.set_ylabel("left coefficient index") + heatmap_ax.set_xticks(range(len(right))) + heatmap_ax.set_yticks(range(len(left))) + + for row, row_values in enumerate(grid): + for column, value in enumerate(row_values): + heatmap_ax.text(column, row, str(value), ha="center", va="center", color="#101010", fontsize=10) + + sum_ax.axis("off") + sum_ax.set_title("Diagonal Sums = Convolution Coefficients", fontsize=12, fontweight="bold", pad=8) + for index, value in enumerate(diagonal_sums): + sum_ax.text( + index, + 0, + f"y{index}\n{value}", + ha="center", + va="center", + fontsize=10, + family="monospace", + bbox={ + "boxstyle": "round,pad=0.35", + "facecolor": "#f4f1de", + "edgecolor": "#222222", + "linewidth": 1.0, + }, + ) + sum_ax.set_xlim(-0.5, len(diagonal_sums) - 0.5) + sum_ax.set_ylim(-1, 1) + fig.tight_layout() + return fig + + +def plot_wraparound( + coefficients: Sequence[int], + n: int, + *, + negacyclic: bool = True, + title: str | None = None, +): + """Plot how the tail wraps back into degree < n.""" + rows = wraparound_contributions(coefficients, n=n, negacyclic=negacyclic) + if title is None: + title = "Negacyclic Folding" if negacyclic else "Cyclic Folding" + + fig, ax = plt.subplots(figsize=(max(8, len(coefficients) * 1.1), 5.5)) + ax.set_title(title, fontsize=14, fontweight="bold") + ax.axis("off") + + top_y = 2.4 + bottom_y = 0.4 + _draw_value_row(ax, coefficients, top_y, "x^") + reduced_values = [row["total"] for row in rows] + _draw_value_row(ax, reduced_values, bottom_y, "slot ") + + for slot, row in enumerate(rows): + for contribution in row["contributions"]: + source_index = contribution["source_index"] + color = "#d1495b" if contribution["sign"] < 0 else "#2a9d8f" + label = "-" if contribution["sign"] < 0 else "+" + ax.annotate( + "", + xy=(slot, bottom_y + 0.3), + xytext=(source_index, top_y - 0.25), + arrowprops={"arrowstyle": "->", "color": color, "linewidth": 2.0}, + ) + mid_x = (slot + source_index) / 2 + mid_y = (top_y + bottom_y) / 2 + 0.25 + ax.text( + mid_x, + mid_y, + f"{label} wrap {contribution['wraps']}", + ha="center", + va="center", + fontsize=9, + color=color, + family="monospace", + ) + + ax.set_xlim(-0.8, max(len(coefficients), n) - 0.2) + ax.set_ylim(-0.4, 3.2) + fig.tight_layout() + return fig + + +def plot_bit_reversal_mapping(length: int, title: str = "Normal Order To Bit-Reversed Order"): + """Plot the bit-reversal permutation as explicit wires.""" + if length <= 0 or length & (length - 1): + raise ValueError("plot_bit_reversal_mapping requires a power-of-two length") + + from .toy_ntt import bit_reversed_indices + + permutation = bit_reversed_indices(length) + width = length.bit_length() - 1 + + fig, ax = plt.subplots(figsize=(8, max(4, length * 0.65))) + ax.set_title(title, fontsize=14, fontweight="bold") + ax.axis("off") + + for index, target in enumerate(permutation): + ax.text( + 0, + -index, + f"{index:>2} | {index:0{width}b}", + ha="center", + va="center", + family="monospace", + bbox={"boxstyle": "round,pad=0.25", "facecolor": "#edf6f9", "edgecolor": "#264653"}, + ) + ax.text( + 4, + -target, + f"{target:>2} | {target:0{width}b}", + ha="center", + va="center", + family="monospace", + bbox={"boxstyle": "round,pad=0.25", "facecolor": "#fff3b0", "edgecolor": "#9c6644"}, + ) + ax.plot([0.6, 3.4], [-index, -target], color="#7f5539", linewidth=2.2, alpha=0.9) + + ax.text(0, 1, "NO", ha="center", va="center", fontsize=12, fontweight="bold") + ax.text(4, 1, "BO", ha="center", va="center", fontsize=12, fontweight="bold") + ax.set_xlim(-1.2, 5.2) + ax.set_ylim(-length + 0.2, 1.8) + fig.tight_layout() + return fig + + +def plot_stage(stage: TransformStage, title: str | None = None): + """Plot one explicit butterfly stage with input and output rows.""" + if title is None: + title = f"{stage.algorithm.upper()} Stage {stage.stage_index}" + + fig, ax = plt.subplots(figsize=(max(8, len(stage.input_values) * 1.35), 5.8)) + ax.set_title(title, fontsize=14, fontweight="bold") + ax.axis("off") + + input_y = 2.6 + output_y = 0.5 + _draw_value_row(ax, stage.input_values, input_y, "i") + _draw_value_row(ax, stage.output_values, output_y, "o") + + colors = ["#264653", "#2a9d8f", "#e76f51", "#8d99ae", "#c1121f", "#3a86ff"] + for pair_index, ((left, right), zeta) in enumerate(zip(stage.pairings, stage.zetas)): + color = colors[pair_index % len(colors)] + center_x = (left + right) / 2 + ax.plot([left, right], [input_y - 0.45, input_y - 0.45], color=color, linewidth=2.5) + ax.plot([left, left], [input_y - 0.45, output_y + 0.55], color=color, linewidth=1.5, alpha=0.85) + ax.plot([right, right], [input_y - 0.45, output_y + 0.55], color=color, linewidth=1.5, alpha=0.85) + ax.text( + center_x, + 1.55, + f"pair {left}-{right}\nzeta={zeta}", + ha="center", + va="center", + fontsize=10, + family="monospace", + bbox={ + "boxstyle": "round,pad=0.35", + "facecolor": "#ffffff", + "edgecolor": color, + "linewidth": 1.4, + }, + ) + + ax.text( + len(stage.input_values) / 2 - 0.5, + -0.05, + stage.note, + ha="center", + va="center", + fontsize=10, + color="#333333", + ) + ax.set_xlim(-0.8, len(stage.input_values) - 0.2) + ax.set_ylim(-0.5, 3.3) + fig.tight_layout() + return fig + + +def plot_trace_overview(trace: TransformTrace, title: str | None = None): + """Plot every stage output as a column of values.""" + if title is None: + title = f"{trace.algorithm.upper()} Trace Overview" + + columns = [trace.input_values] + [stage.output_values for stage in trace.stages] + fig, ax = plt.subplots(figsize=(max(9, len(columns) * 2.0), max(4.5, len(trace.input_values) * 0.7))) + ax.set_title(title, fontsize=14, fontweight="bold") + ax.axis("off") + + for column_index, values in enumerate(columns): + x = column_index * 2.0 + for row_index, value in enumerate(values): + ax.text( + x, + -row_index, + str(value), + ha="center", + va="center", + fontsize=10, + family="monospace", + bbox={ + "boxstyle": "round,pad=0.25", + "facecolor": _value_colors([value])[0], + "edgecolor": "#222222", + "linewidth": 1.0, + }, + ) + if column_index == 0: + label = "input" + else: + label = f"stage {column_index}" + ax.text(x, 1, label, ha="center", va="center", fontsize=11, fontweight="bold") + + ax.set_xlim(-1.0, (len(columns) - 1) * 2.0 + 1.0) + ax.set_ylim(-len(trace.input_values) + 0.2, 1.8) + fig.tight_layout() + return fig + + +def interactive_trace(trace: TransformTrace, title: str | None = None): + """Return a slider-based stage explorer for a transform trace.""" + if title is None: + title = f"{trace.algorithm.upper()} Stage Explorer" + + slider = widgets.IntSlider( + value=1, + min=1, + max=max(1, len(trace.stages)), + step=1, + description="Stage", + continuous_update=False, + ) + output = widgets.Output() + + def render(stage_index: int) -> None: + with output: + clear_output(wait=True) + stage = trace.stages[stage_index - 1] + fig = plot_stage(stage, title=f"{title} | Stage {stage_index}") + display(fig) + plt.close(fig) + rows = [] + for pair, zeta in zip(stage.pairings, stage.zetas): + left, right = pair + rows.append( + f"pair {pair}: inputs=({stage.input_values[left]}, {stage.input_values[right]}) " + f"-> outputs=({stage.output_values[left]}, {stage.output_values[right]}) | zeta={zeta}" + ) + print("\n".join(rows)) + + slider.observe(lambda change: render(change["new"]), names="value") + render(slider.value) + widget = widgets.VBox([widgets.HTML(f"

{title}

"), slider, output]) + return widget + + +def show_trace(trace: TransformTrace, title: str | None = None): + """Display the interactive trace widget immediately.""" + widget = interactive_trace(trace, title=title) + display(widget) + return widget diff --git a/tests/test_notebook_execution.py b/tests/test_notebook_execution.py index d891748..10860ee 100644 --- a/tests/test_notebook_execution.py +++ b/tests/test_notebook_execution.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib import io import json +import os import sys import unittest from pathlib import Path @@ -12,6 +13,12 @@ from ntt_learning.course import REPO_ROOT, TECHNICAL_NOTEBOOKS if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +MPLCONFIGDIR = REPO_ROOT / ".cache" / "matplotlib" +MPLCONFIGDIR.mkdir(parents=True, exist_ok=True) +os.environ.setdefault("MPLCONFIGDIR", str(MPLCONFIGDIR)) + +import matplotlib.pyplot as plt + def read_notebook(relative_path: Path) -> dict[str, object]: return json.loads((REPO_ROOT / relative_path).read_text(encoding="utf-8")) @@ -46,8 +53,8 @@ class NotebookExecutionTests(unittest.TestCase): mode="exec", ) exec(code_object, namespace, namespace) + plt.close("all") if __name__ == "__main__": unittest.main() - diff --git a/tests/test_toy_ntt.py b/tests/test_toy_ntt.py index 5456419..1f11664 100644 --- a/tests/test_toy_ntt.py +++ b/tests/test_toy_ntt.py @@ -4,12 +4,21 @@ import unittest from ntt_learning.toy_ntt import ( apply_ct_stage, + base_multiply_pair, bit_reverse, + bit_reversed_indices, bit_reversed_order, ct_butterfly_pair, + fast_intt_psi_gs, + fast_intt_psi_gs_trace, + fast_ntt_psi_ct, + fast_ntt_psi_ct_trace, + find_psi, find_primitive_root, forward_ntt, + forward_ntt_psi, inverse_ntt, + inverse_ntt_psi, negacyclic_multiply, negacyclic_reduce, schoolbook_convolution, @@ -53,6 +62,37 @@ class ToyNttTests(unittest.TestCase): def test_bit_reversed_order(self) -> None: self.assertEqual(bit_reversed_order(list(range(8))), [0, 4, 2, 6, 1, 5, 3, 7]) + def test_bit_reversed_indices(self) -> None: + self.assertEqual(bit_reversed_indices(8), [0, 4, 2, 6, 1, 5, 3, 7]) + + def test_find_psi(self) -> None: + self.assertEqual(find_psi(order=4, modulus=17), 2) + + def test_forward_inverse_ntt_psi_round_trip(self) -> None: + signal = [6, 0, 5, 2] + psi = find_psi(order=4, modulus=17) + spectrum = forward_ntt_psi(signal, modulus=17, psi=psi) + self.assertEqual(inverse_ntt_psi(spectrum, modulus=17, psi=psi), signal) + + def test_fast_ct_trace_matches_paper_example(self) -> None: + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], modulus=7681, psi=1925) + self.assertEqual(list(trace.raw_output), [1467, 3471, 2807, 7621]) + self.assertEqual(list(trace.normal_order_output), [1467, 2807, 3471, 7621]) + + def test_fast_gs_trace_matches_paper_example(self) -> None: + trace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], modulus=7681, psi=1925) + self.assertEqual(list(trace.raw_output), [4, 8, 12, 16]) + self.assertEqual(list(trace.scaled_output or ()), [1, 2, 3, 4]) + + def test_fast_ct_and_gs_round_trip(self) -> None: + signal = [3, 1, 4, 1] + psi = find_psi(order=4, modulus=17) + bo_spectrum = fast_ntt_psi_ct(signal, modulus=17, psi=psi) + self.assertEqual(fast_intt_psi_gs(bo_spectrum, modulus=17, psi=psi), signal) + + def test_base_multiply_pair(self) -> None: + self.assertEqual(base_multiply_pair([1, 2], [3, 4], zeta=5, modulus=17), [9, 10]) + if __name__ == "__main__": unittest.main() diff --git a/tools/render_notebooks.py b/tools/render_notebooks.py index 9fc3790..95893db 100644 --- a/tools/render_notebooks.py +++ b/tools/render_notebooks.py @@ -11,11 +11,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from ntt_learning.course import FOUNDATION_BUNDLE_DIR, NOTEBOOK_SEQUENCE - - -def block(text: str) -> str: - return dedent(text).strip() + "\n" +from ntt_learning.course import BUNDLE_DIRS, NOTEBOOK_SEQUENCE def normalized_body(text: str) -> str: @@ -68,7 +64,7 @@ def notebook(title: str, cells: list[dict[str, object]]) -> dict[str, object]: "language_info": {"name": "python"}, "ntt_learning": { "title": title, - "contract_version": "0.1", + "contract_version": "0.2", "sequence": [str(path) for path in NOTEBOOK_SEQUENCE], }, }, @@ -83,9 +79,19 @@ def write_notebook(relative_path: str, payload: dict[str, object]) -> None: destination.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") -def build_notebooks() -> None: - foundation_link = FOUNDATION_BUNDLE_DIR.as_posix() +def handoff_cell(next_notebook: str) -> dict[str, object]: + return markdown("meta", 1, "handoff", "Next Notebook", f"Next notebook: `{next_notebook}`") + +def write_bundle(bundle_dir: Path, bundle_title: str, lecture: list[dict[str, object]], lab: list[dict[str, object]], problems: list[dict[str, object]], studio: list[dict[str, object]]) -> None: + relative = bundle_dir.as_posix() + write_notebook(f"{relative}/lecture.ipynb", notebook(f"Lecture: {bundle_title}", lecture)) + write_notebook(f"{relative}/lab.ipynb", notebook(f"Lab: {bundle_title}", lab)) + write_notebook(f"{relative}/problems.ipynb", notebook(f"Problems: {bundle_title}", problems)) + write_notebook(f"{relative}/studio.ipynb", notebook(f"Studio: {bundle_title}", studio)) + + +def build_start_here() -> None: write_notebook( "notebooks/START_HERE.ipynb", notebook( @@ -97,14 +103,14 @@ def build_notebooks() -> None: "orientation", "Welcome", """ - This course is local-first and notebook-first. Every visible cell is labeled so the learner always knows - whether a cell is route guidance, required walkthrough material, or an optional detour. + This course is for people who need to see the algorithm move. - Contract: + The goal is not to hide behind abstract formulas. The goal is to make the NTT and iNTT feel physically inspectable: - - `META` = route, pacing, and handoff guidance - - `MANDATORY` = the official walkthrough - - `FACULTATIVE` = optional extension + - every stage should look like values moving through wires + - every wraparound should be visible + - every ordering change should be concrete + - every Kyber-specific choice should be motivated by what the arithmetic allows """, ), markdown( @@ -113,17 +119,37 @@ def build_notebooks() -> None: "route", "Official Route", """ - Follow exactly one supported path: + Follow exactly one supported route: 1. `START_HERE.ipynb` 2. `COURSE_BLUEPRINT.ipynb` - 3. `{foundation_link}/lecture.ipynb` - 4. `{foundation_link}/lab.ipynb` - 5. `{foundation_link}/problems.ipynb` - 6. `{foundation_link}/studio.ipynb` + 3. each bundle in `Lecture -> Lab -> Problems -> Studio` order + 4. `COURSE_COMPLETE.ipynb` - The course starts with concrete arrays and small examples before any Kyber-specific implementation details. - """.format(foundation_link=foundation_link), + Supported bundles: + + - `foundations/01_convolution_to_toy_ntt` + - `foundations/02_negative_wrapped_ntt` + - `butterfly_mechanics/03_fast_forward_ct` + - `butterfly_mechanics/04_fast_inverse_gs` + - `kyber_mapping/05_kyber_ntt_and_base_multiplication` + - `professional/06_debugging_ntt_failures` + """, + ), + markdown( + "mandatory", + 1, + "contract", + "Visible Cell Contract", + """ + Cell labels are not decoration. They tell you how to use the notebook: + + - `META` = route, pacing, and handoff + - `MANDATORY` = the official walkthrough + - `FACULTATIVE` = optional deepening only + - difficulty `1-3` is reserved for mandatory work + - difficulty `4-10` is reserved for facultative work + """, ), markdown( "meta", @@ -133,24 +159,19 @@ def build_notebooks() -> None: """ Repo-local commands: - - `scripts/bootstrap.sh` creates `.venv` and installs dependencies - - `scripts/validate.sh` runs structural and execution checks - - `scripts/start.sh` launches JupyterLab when it is installed - """, - ), - markdown( - "meta", - 1, - "handoff", - "Next Notebook", - """ - Next notebook: `COURSE_BLUEPRINT.ipynb` + - `scripts/bootstrap.sh` + - `scripts/start.sh` + - `scripts/status.sh` + - `scripts/validate.sh` """, ), + handoff_cell("COURSE_BLUEPRINT.ipynb"), ], ), ) + +def build_course_blueprint() -> None: write_notebook( "notebooks/COURSE_BLUEPRINT.ipynb", notebook( @@ -160,577 +181,2727 @@ def build_notebooks() -> None: "meta", 1, "orientation", - "Why This Order Exists", + "What This Course Separates", """ - The course separates three ideas that are often blurred together: + This course keeps three stories separate on purpose: - - the algebraic purpose of the NTT - - the in-place butterfly dataflow - - Kyber-specific implementation conventions - """, - ), - markdown( - "mandatory", - 2, - "route", - "Learning Staircase", - """ - The supported staircase is: + - the algebraic purpose of the transform + - the local in-place butterfly dataflow + - the Kyber-specific implementation conventions - 1. ordinary polynomial multiplication and convolution - 2. negacyclic multiplication - 3. a tiny toy NTT - 4. butterfly mechanics in isolation - 5. forward and inverse flow side by side - 6. Kyber-specific parameters and indexing - 7. real implementation patterns + The point is to stop those three from collapsing into one blurry “FFT-like thing”. """, ), markdown( "mandatory", 2, "structure", - "Bundle Rhythm", + "The Learning Staircase", """ - Technical bundles follow a consistent rhythm: + The supported staircase is: - - `lecture.ipynb` explains the idea carefully - - `lab.ipynb` asks for predictions before execution - - `problems.ipynb` checks retrieval and reflection - - `studio.ipynb` compares implementation choices and debugging cues + 1. schoolbook multiplication and diagonals + 2. cyclic and negacyclic wraparound + 3. direct negative-wrapped NTT and iNTT + 4. fast forward CT butterflies + 5. fast inverse GS butterflies + 6. bit-reversal and ordering + 7. Kyber parameter reality and base multiplication + 8. debugging wrong sign / wrong zeta / wrong order / wrong scale failures """, ), markdown( - "meta", - 1, - "constraints", - "Route Constraints", + "mandatory", + 2, + "bundles", + "Bundles", """ - Route notebooks stay pure route notebooks. + Each serious module uses the same rhythm: - - no facultative detours here - - no hidden competing learner route - - every notebook ends with a visible handoff + - `lecture.ipynb` = slow explanation plus visual demos + - `lab.ipynb` = prediction before execution + - `problems.ipynb` = retrieval and reflection + - `studio.ipynb` = comparison, debugging, and implementation reading """, ), markdown( - "meta", - 1, - "handoff", - "Next Notebook", + "mandatory", + 2, + "expectation", + "What “Blunt And Graphical” Means Here", """ - Next notebook: `foundations/01_convolution_to_toy_ntt/lecture.ipynb` + The notebooks should not ask the learner to imagine too much in their head. + + Expect: + + - schoolbook product grids + - wraparound arrows + - explicit stage arrays + - stage sliders + - bit-reversal wire maps + - side-by-side wrong vs right traces """, ), + handoff_cell("foundations/01_convolution_to_toy_ntt/lecture.ipynb"), ], ), ) + +def build_course_complete() -> None: write_notebook( - "notebooks/foundations/01_convolution_to_toy_ntt/lecture.ipynb", + "notebooks/COURSE_COMPLETE.ipynb", notebook( - "Lecture: Convolution To Toy NTT", + "Course Complete", [ markdown( "meta", 1, "orientation", - "Objectives", + "Route Complete", """ - This notebook introduces the first technical bundle. + You reached the end of the supported route. - Focus: + If the course did its job, you should now be able to separate: - - why polynomial multiplication matters - - what negacyclic folding changes - - how a tiny toy NTT gives a matrix-level view - - what a butterfly does locally - """, - ), - markdown( - "mandatory", - 2, - "explanation", - "Convolution Before Transforms", - """ - Start with the concrete problem. Two coefficient arrays multiply by accumulating all pairwise products. - That schoolbook view is the baseline the learner should be able to inspect by hand before a transform is introduced. - """, - ), - code( - "mandatory", - 2, - "demo", - "Inspect Convolution And Negacyclic Folding", - """ - from ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution - - left = [2, 1, 3, 0] - right = [1, 4, 0, 2] - - print("convolution:", schoolbook_convolution(left, right)) - print("negacyclic in x^4 + 1:", negacyclic_multiply(left, right, n=4)) - """, - ), - markdown( - "mandatory", - 2, - "explanation", - "Toy NTT As A Round Trip", - """ - A tiny transform is useful because it keeps every entry inspectable. The first goal is not Kyber fidelity. - The first goal is to see that the transform maps one coefficient view to another and can be inverted. - """, - ), - code( - "mandatory", - 2, - "demo", - "Run A Tiny Forward And Inverse NTT", - """ - from ntt_learning.toy_ntt import find_primitive_root, forward_ntt, inverse_ntt - - modulus = 17 - omega = find_primitive_root(order=4, modulus=modulus) - signal = [3, 1, 4, 1] - spectrum = forward_ntt(signal, modulus=modulus, omega=omega) - - print("primitive 4th root:", omega) - print("forward spectrum:", spectrum) - print("inverse recovery:", inverse_ntt(spectrum, modulus=modulus, omega=omega)) - """, - ), - markdown( - "mandatory", - 3, - "explanation", - "Butterflies Are Local Dataflow", - """ - A butterfly is a local rewrite of a pair. The pair changes because one branch is twiddled by a zeta value. - This is separate from the global story about polynomial multiplication. - - Forward Cooley-Tukey and inverse Gentleman-Sande have the same shape intuition: pair values, combine them, and move layer by layer. - """, - ), - code( - "mandatory", - 3, - "demo", - "Compare Pairwise And Stage-Level Butterfly Views", - """ - from ntt_learning.toy_ntt import action_rows, apply_ct_stage, ct_butterfly_pair, gs_butterfly_pair - - print("single CT pair:", ct_butterfly_pair(top=7, bottom=5, zeta=3, modulus=17)) - print("single GS pair:", gs_butterfly_pair(top=7, bottom=5, zeta=3, modulus=17)) - - values = [3, 1, 4, 1] - stage_output, stage_actions = apply_ct_stage(values, block_size=2, zetas=1, modulus=17) - - print("stage output:", stage_output) - print("stage trace:", action_rows(stage_actions)) - """, - ), - markdown( - "mandatory", - 2, - "quiz", - "Retrieval Check", - """ - Quiz: - - 1. What changes when schoolbook multiplication is folded negacyclically? - 2. Why is the toy NTT introduced before Kyber indexing details? - 3. In a butterfly, which part of the computation is local and directly inspectable? - """, - ), - markdown( - "facultative", - 4, - "exploration", - "Optional Extension", - """ - If the local pairings already feel comfortable, inspect a bit-reversed ordering next. That prepares the learner - for later discussions of array ordering without mixing it into the mandatory route too early. - """, - ), - code( - "facultative", - 4, - "exploration", - "Bit-Reversed Ordering", - """ - from ntt_learning.toy_ntt import bit_reversed_order - - print(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7])) - """, - ), - markdown( - "meta", - 1, - "handoff", - "Next Notebook", - """ - Next notebook: `lab.ipynb` - """, - ), - ], - ), - ) - - write_notebook( - "notebooks/foundations/01_convolution_to_toy_ntt/lab.ipynb", - notebook( - "Lab: Convolution To Toy NTT", - [ - markdown( - "meta", - 1, - "orientation", - "Lab Goals", - """ - This lab asks for prediction before execution. - - The learner should pause and name the expected pairings and sign changes before reading the output. - """, - ), - markdown( - "mandatory", - 2, - "exercise", - "Exercise 1", - """ - Before running the next cell, predict which coefficients will collide when the raw convolution is folded into `x^4 + 1`. - """, - ), - code( - "mandatory", - 2, - "exercise", - "Work Two Multiplication Examples", - """ - from ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution - - samples = [ - ([1, 2, 0, 0], [3, 4, 0, 0]), - ([5, 0, 1, 2], [2, 1, 0, 1]), - ] - - for left, right in samples: - print("left:", left, "right:", right) - print(" convolution:", schoolbook_convolution(left, right)) - print(" negacyclic:", negacyclic_multiply(left, right, n=4)) - """, - ), - markdown( - "mandatory", - 3, - "exercise", - "Exercise 2", - """ - Predict the pairings for a single Cooley-Tukey stage on eight values with block size four. - Name the index pairs before running the cell. - """, - ), - code( - "mandatory", - 3, - "exercise", - "Trace One Butterfly Layer", - """ - from ntt_learning.toy_ntt import action_rows, apply_ct_stage - - values = [0, 1, 2, 3, 4, 5, 6, 7] - stage_output, stage_actions = apply_ct_stage( - values, - block_size=4, - zetas=[1, 4, 1, 4], - modulus=17, - ) - - print("stage output:", stage_output) - for row in action_rows(stage_actions): - print(row) + - raw polynomial multiplication + - negacyclic structure + - direct NTT / iNTT definitions + - fast CT / GS butterfly flow + - order changes and scaling + - Kyber’s specific modulus and base-multiplication story """, ), markdown( "mandatory", 2, "reflection", - "Reflection", + "Exit Reflection", """ - Reflection prompt: + Final written prompts: - - Which part of the stage felt mechanical and local? - - Which part still feels global or mysterious? - - If one zeta is wrong, what kind of output difference would you expect to see? + 1. Explain the difference between “the transform as mathematics” and “the butterfly network as an implementation strategy”. + 2. Explain why Kyber v3 needs more than the naive “just use a 2n-th root” mental model. + 3. Name the first four checks you would run if an iNTT output looked wrong. """, ), - code( - "facultative", - 4, - "exploration", - "Optional Inverse-Style Stage", + markdown( + "meta", + 1, + "handoff", + "Next Notebook", """ - from ntt_learning.toy_ntt import action_rows, apply_gs_stage + Next notebook: you are at the end of the supported route. Revisit the studios if you want more repetition. + """, + ), + ], + ), + ) - values = [5, 1, 9, 3, 7, 2, 6, 4] - stage_output, stage_actions = apply_gs_stage( - values, - block_size=4, - zetas=[1, 4, 1, 4], - modulus=17, + +def build_bundle_01() -> None: + bundle_dir = BUNDLE_DIRS[0] + write_bundle( + bundle_dir, + "Convolution To Toy NTT", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This first bundle is about making the raw multiplication problem visible before any transform is introduced. + + Focus: + + - the schoolbook product grid + - diagonal sums + - cyclic vs negacyclic folding + - a tiny teaser of why transforms help + """, + ), + markdown( + "mandatory", + 2, + "explanation", + "Schoolbook Multiplication Is A Grid", + """ + Stop thinking “multiply two polynomials” as one sentence. + The mechanical reality is a grid of pairwise products whose diagonals have to be accumulated. + + If that grid is not concrete, the NTT has nothing to optimize in your mind. + """, + ), + code( + "mandatory", + 2, + "demo", + "See The Product Grid And The Diagonal Sums", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import convolution_contributions, schoolbook_convolution + from ntt_learning.visuals import plot_convolution_grid + + left = [1, 2, 3, 4] + right = [5, 6, 7, 8] + raw = schoolbook_convolution(left, right) + + print("raw convolution:", raw) + for row in convolution_contributions(left, right): + print(row) + + fig = plot_convolution_grid(left, right, title="Schoolbook products for [1,2,3,4] * [5,6,7,8]") + display(fig) + """, + ), + markdown( + "mandatory", + 2, + "explanation", + "Wraparound Is The First Structural Fork", + """ + Once the raw tail exists, the ring tells you what to do with it. + + - in `x^n - 1`, high-degree terms wrap back with a positive sign + - in `x^n + 1`, high-degree terms wrap back with a sign flip + + That sign flip is not cosmetic. It is exactly what makes the negacyclic story different. + """, + ), + code( + "mandatory", + 2, + "demo", + "See Cyclic And Negacyclic Folding Side By Side", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution, wraparound_contributions + from ntt_learning.visuals import plot_wraparound + + left = [1, 2, 3, 4] + right = [5, 6, 7, 8] + raw = schoolbook_convolution(left, right) + + print("raw convolution:", raw) + print("negacyclic in x^4 + 1:", negacyclic_multiply(left, right, n=4)) + print("cyclic folding rows:") + for row in wraparound_contributions(raw, n=4, negacyclic=False): + print(row) + print("negacyclic folding rows:") + for row in wraparound_contributions(raw, n=4, negacyclic=True): + print(row) + + display(plot_wraparound(raw, n=4, negacyclic=False, title="Cyclic folding into x^4 - 1")) + display(plot_wraparound(raw, n=4, negacyclic=True, title="Negacyclic folding into x^4 + 1")) + """, + ), + markdown( + "mandatory", + 2, + "explanation", + "The Tiny Transform Teaser", + """ + The transform is not magic. It is a change of coordinates chosen so that multiplication gets easier. + + The next bundle will treat the transform itself directly. + This first bundle only makes sure the learner can see the raw thing being optimized. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + Answer in words before moving on: + + 1. Why do diagonal sums appear in schoolbook multiplication? + 2. What is the one exact sign difference between cyclic and negacyclic folding? + 3. If you cannot track the tail wraparound, what part of the NTT story will stay vague? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Compare Another Example", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution + from ntt_learning.visuals import plot_convolution_grid, plot_wraparound + + left = [2, 1, 0, 3] + right = [4, 0, 1, 2] + raw = schoolbook_convolution(left, right) + + print("raw convolution:", raw) + print("negacyclic:", negacyclic_multiply(left, right, n=4)) + display(plot_convolution_grid(left, right, title="A second schoolbook grid")) + display(plot_wraparound(raw, n=4, negacyclic=True, title="A second negacyclic fold")) + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + Predict the movement before you run the code. + + The point is not just to see the picture after the fact. + The point is to force your eye to anticipate where the products and wraparound terms will land. + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Exercise 1", + """ + Before running the next cell: + + - name the diagonal sums of the raw schoolbook grid + - say which tail terms will wrap into slot `0` + - say whether they add or subtract in `x^4 + 1` + """, + ), + code( + "mandatory", + 2, + "exercise", + "Run The Prediction Check", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import negacyclic_multiply, schoolbook_convolution + from ntt_learning.visuals import plot_convolution_grid, plot_wraparound + + left = [3, 0, 2, 1] + right = [1, 4, 0, 2] + raw = schoolbook_convolution(left, right) + + print("raw convolution:", raw) + print("negacyclic result:", negacyclic_multiply(left, right, n=4)) + display(plot_convolution_grid(left, right, title="Prediction check grid")) + display(plot_wraparound(raw, n=4, negacyclic=True, title="Prediction check fold")) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Exercise 2", + """ + Pick one number in the raw tail and follow it all the way to its final slot. + Do not say “it wraps around”. + Say exactly: + + - where it started + - how many wraps happened + - whether the sign flipped + - where it finished + """, + ), + code( + "mandatory", + 2, + "exercise", + "A Second Visual Drill", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import schoolbook_convolution + from ntt_learning.visuals import plot_wraparound + + raw = schoolbook_convolution([2, 5, 0, 1], [1, 0, 3, 2]) + print("raw convolution:", raw) + display(plot_wraparound(raw, n=4, negacyclic=True, title="Trace one tail coefficient by eye")) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - What felt easier to see in the grid than in symbolic polynomial notation? + - What exactly makes negacyclic folding more annoying than ordinary wraparound? + - If you had to explain `x^n + 1` to somebody visually, what would you draw? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Try Your Own Arrays", + """ + import ipywidgets as widgets + from IPython.display import display + + from ntt_learning.toy_ntt import schoolbook_convolution + from ntt_learning.visuals import plot_convolution_grid, plot_wraparound + + def preview(a0=1, a1=2, a2=3, a3=4, b0=5, b1=6, b2=7, b3=8): + left = [a0, a1, a2, a3] + right = [b0, b1, b2, b3] + raw = schoolbook_convolution(left, right) + display(plot_convolution_grid(left, right, title="Interactive schoolbook grid")) + display(plot_wraparound(raw, n=4, negacyclic=True, title="Interactive negacyclic fold")) + + display( + widgets.interact( + preview, + a0=(0, 6), + a1=(0, 6), + a2=(0, 6), + a3=(0, 6), + b0=(0, 6), + b1=(0, 6), + b2=(0, 6), + b3=(0, 6), ) + ) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the multiplication and folding pictures are now stable in memory. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: - print("stage output:", stage_output) - for row in action_rows(stage_actions): + 1. The diagonal sums in schoolbook multiplication come from: + A. random coincidence + B. grouping terms with the same final degree + C. bit-reversal + + 2. Negacyclic folding differs from cyclic folding because: + A. the wrapped tail flips sign + B. the polynomial degrees disappear + C. the raw convolution gets shorter before folding + + 3. The main reason to study the raw grid before NTT is: + A. because the transform is impossible otherwise + B. because it makes the optimized algorithm visually grounded + C. because Kyber never uses transforms + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "A", 3: "B"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Manual Fold Check", + """ + Compute the negacyclic fold of the raw vector `[5, 16, 34, 60, 61, 52, 32]` into `x^4 + 1` by hand before running the next cell. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Check The Fold", + """ + from ntt_learning.toy_ntt import negacyclic_reduce + + raw = [5, 16, 34, 60, 61, 52, 32] + print(negacyclic_reduce(raw, n=4)) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + In one paragraph, explain why “wrap the tail back” is still too vague unless you also specify: + + - the divisor + - the target slot + - the sign rule + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + from ntt_learning.toy_ntt import wraparound_contributions + + raw = [3, 11, 7, 0, 5, 9, 4] + for row in wraparound_contributions(raw, n=4, negacyclic=True): + print(row) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + This studio is about comparison and diagnosis. + The learner should leave with a strong visual distinction between cyclic and negacyclic wraparound. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Two Folds, Same Raw Tail, Different Result", + """ + If the raw convolution is fixed, the only thing that changes is the ring rule. + That is exactly why the same tail can produce two different reduced polynomials. + """, + ), + code( + "mandatory", + 3, + "demo", + "Compare The Two Fold Rules", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import schoolbook_convolution + from ntt_learning.visuals import plot_wraparound + + raw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8]) + print("raw convolution:", raw) + display(plot_wraparound(raw, n=4, negacyclic=False, title="Positive wrap into x^4 - 1")) + display(plot_wraparound(raw, n=4, negacyclic=True, title="Negative wrap into x^4 + 1")) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Checklist", + """ + If a wraparound result looks wrong, inspect these in order: + + 1. Was the raw convolution itself correct? + 2. Was the divisor `x^n - 1` or `x^n + 1`? + 3. Did the wrapped tail land in the right slot? + 4. Did the sign flip happen on the wrapped term? + """, + ), + code( + "mandatory", + 2, + "exercise", + "See A Wrong-Sign Failure", + """ + from ntt_learning.toy_ntt import schoolbook_convolution, negacyclic_reduce + + raw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8]) + wrong = [raw[0] + raw[4], raw[1] + raw[5], raw[2] + raw[6], raw[3]] + + print("raw:", raw) + print("wrong sign fold:", wrong) + print("correct negacyclic fold:", negacyclic_reduce(raw, n=4)) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Explain the exact visual difference between “the wrong-sign fold” and “the correct negacyclic fold”. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Fold A Larger Tail", + """ + from IPython.display import display + + from ntt_learning.visuals import plot_wraparound + + raw = [4, 8, 12, 16, 9, 5, 1, 7, 11] + display(plot_wraparound(raw, n=4, negacyclic=True, title="Longer tail, same fold rule")) + """, + ), + handoff_cell("../../../foundations/02_negative_wrapped_ntt/lecture.ipynb"), + ], + ) + + +def build_bundle_02() -> None: + bundle_dir = BUNDLE_DIRS[1] + write_bundle( + bundle_dir, + "Negative-Wrapped NTT", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This bundle introduces the transform itself in its negacyclic form. + + Focus: + + - the difference between `ω` and `ψ` + - direct NTTψ and INTTψ + - the direct convolution theorem in the negacyclic setting + - why this is still too slow at `O(n^2)` without butterflies + """, + ), + markdown( + "mandatory", + 2, + "explanation", + "Why ψ Shows Up", + """ + For negative-wrapped convolution, the clean transform formula uses a `2n`-th root `ψ` with: + + - `ψ^2 = ω` + - `ψ^n = -1` + + That is what bakes the negacyclic sign rule into the transform itself. + """, + ), + code( + "mandatory", + 2, + "demo", + "Inspect ω, ψ, And The Direct Transform Matrix", + """ + from ntt_learning.toy_ntt import find_primitive_root, find_psi, ntt_psi_exponent_grid, ntt_psi_matrix + + modulus = 17 + n = 4 + omega = find_primitive_root(n, modulus) + psi = find_psi(n, modulus) + + print("omega:", omega) + print("psi:", psi) + print("exponent grid:") + for row in ntt_psi_exponent_grid(n): + print(row) + print("NTT_psi matrix:") + for row in ntt_psi_matrix(n, modulus, psi): + print(row) + """, + ), + markdown( + "mandatory", + 2, + "explanation", + "Direct NTTψ Is Mechanically Clear But Still Quadratic", + """ + The direct transform is useful because every coefficient and every exponent is visible. + It is not yet efficient. It still performs the full matrix multiplication. + """, + ), + code( + "mandatory", + 2, + "demo", + "Run A Direct NTTψ / INTTψ Round Trip", + """ + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi + + signal = [1, 2, 3, 4] + modulus = 17 + psi = find_psi(len(signal), modulus) + spectrum = forward_ntt_psi(signal, modulus, psi) + + print("signal:", signal) + print("spectrum:", spectrum) + print("inverse recovery:", inverse_ntt_psi(spectrum, modulus, psi)) + """, + ), + code( + "mandatory", + 3, + "demo", + "Use Direct NTTψ For Negacyclic Multiplication", + """ + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi, negacyclic_multiply, pointwise_multiply + + left = [1, 2, 3, 4] + right = [5, 6, 7, 8] + modulus = 17 + psi = find_psi(4, modulus) + + left_hat = forward_ntt_psi(left, modulus, psi) + right_hat = forward_ntt_psi(right, modulus, psi) + product_hat = pointwise_multiply(left_hat, right_hat, modulus) + + print("NTT_psi(left):", left_hat) + print("NTT_psi(right):", right_hat) + print("pointwise product:", product_hat) + print("inverse of pointwise product:", inverse_ntt_psi(product_hat, modulus, psi)) + print("schoolbook negacyclic:", negacyclic_multiply(left, right, n=4, modulus=modulus)) + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + 1. Why is `ψ` stronger than `ω` in the negacyclic story? + 2. What exact property does the inverse add that the forward transform does not? + 3. Why are we still dissatisfied after seeing the direct transform work correctly? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Compare Positive And Negative Wrapped Transforms", + """ + from ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt, forward_ntt_psi + + signal = [1, 2, 3, 4] + modulus = 17 + omega = find_primitive_root(4, modulus) + psi = find_psi(4, modulus) + + print("positive-wrapped NTT:", forward_ntt(signal, modulus, omega)) + print("negative-wrapped NTT_psi:", forward_ntt_psi(signal, modulus, psi)) + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + The lab is about prediction inside the direct transform matrix. + Do not run the next cells until you name the powers and products you expect to matter. + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Exercise 1", + """ + For `signal = [1,2,3,4]`, `n = 4`, `q = 17`, predict: + + - which powers of `ψ` appear in row `j = 1` + - whether the inverse should need both `ψ^-1` and `n^-1` + """, + ), + code( + "mandatory", + 2, + "exercise", + "Prediction Check", + """ + from ntt_learning.toy_ntt import find_psi, ntt_psi_exponent_grid + + psi = find_psi(4, 17) + print("psi:", psi) + for row_index, row in enumerate(ntt_psi_exponent_grid(4)): + print("row", row_index, row) + """, + ), + code( + "mandatory", + 3, + "exercise", + "Interactive Signal Explorer", + """ + import ipywidgets as widgets + from IPython.display import display + + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi + + modulus = 17 + psi = find_psi(4, modulus) + + def preview(a0=1, a1=2, a2=3, a3=4): + signal = [a0, a1, a2, a3] + spectrum = forward_ntt_psi(signal, modulus, psi) + print("signal:", signal) + print("spectrum:", spectrum) + print("inverse:", inverse_ntt_psi(spectrum, modulus, psi)) + + display( + widgets.interact( + preview, + a0=(0, 16), + a1=(0, 16), + a2=(0, 16), + a3=(0, 16), + ) + ) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Exercise 2", + """ + Explain what the pointwise multiplication in the transform domain is buying you. + Use the words “replace convolution by slotwise multiplication” in your own sentence. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Compare Raw Convolution And Slotwise Multiplication", + """ + from ntt_learning.toy_ntt import ( + find_psi, + forward_ntt_psi, + inverse_ntt_psi, + pointwise_multiply, + schoolbook_convolution, + ) + + left = [2, 1, 0, 3] + right = [4, 0, 1, 2] + psi = find_psi(4, 17) + left_hat = forward_ntt_psi(left, 17, psi) + right_hat = forward_ntt_psi(right, 17, psi) + + print("schoolbook raw:", schoolbook_convolution(left, right)) + print("left_hat:", left_hat) + print("right_hat:", right_hat) + print("pointwise product:", pointwise_multiply(left_hat, right_hat, 17)) + print("inverse:", inverse_ntt_psi(pointwise_multiply(left_hat, right_hat, 17), 17, psi)) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - What feels concrete in the direct transform matrix? + - What still feels too expensive or repetitive? + - Why are butterflies the obvious next step? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Try A Different Modulus", + """ + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi + + modulus = 97 + psi = find_psi(4, modulus) + signal = [1, 2, 3, 4] + + print("modulus:", modulus) + print("psi:", psi) + print("spectrum:", forward_ntt_psi(signal, modulus, psi)) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the direct negative-wrapped transform is now mechanically understandable. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: + + 1. In the negacyclic transform, `ψ` matters because: + A. it makes the modulus disappear + B. it encodes the `x^n + 1` sign structure + C. it avoids all inverses + + 2. The inverse transform differs from the forward transform by: + A. an inverse root and an `n^-1` scaling + B. a larger modulus + C. removing all twiddle factors + + 3. The direct matrix transform is still pedagogically useful because: + A. it keeps every coefficient contribution visible + B. it is how Kyber is implemented directly at full size + C. it removes the need for butterflies + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "A", 3: "A"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Round-Trip Check", + """ + Verify by code that `INTTψ(NTTψ(a)) = a` for a nontrivial vector. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Check The Round Trip", + """ + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi, inverse_ntt_psi + + signal = [6, 0, 5, 2] + psi = find_psi(4, 17) + recovered = inverse_ntt_psi(forward_ntt_psi(signal, 17, psi), 17, psi) + print(recovered) + assert recovered == signal + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + In one paragraph, explain why the direct transform is the right place to understand the algebra, but not the right place to stop if you care about algorithmic speed. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + from ntt_learning.toy_ntt import find_psi, forward_ntt_psi + + psi = find_psi(4, 17) + for signal in ([1, 1, 1, 1], [0, 1, 0, 1], [3, 5, 7, 9]): + print(signal, "->", forward_ntt_psi(signal, 17, psi)) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + The studio compares direct positive-wrapped and negative-wrapped transforms so the learner stops treating “NTT” as one unqualified object. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Same Input, Different Transform Story", + """ + The same coefficient vector can be sent through two different transform stories depending on the quotient ring. + That difference is not implementation noise. It is structural. + """, + ), + code( + "mandatory", + 3, + "demo", + "Compare Positive-Wrapped And Negative-Wrapped Views", + """ + from ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt, forward_ntt_psi + + signal = [1, 2, 3, 4] + modulus = 17 + omega = find_primitive_root(4, modulus) + psi = find_psi(4, modulus) + + print("positive-wrapped NTT:", forward_ntt(signal, modulus, omega)) + print("negative-wrapped NTT_psi:", forward_ntt_psi(signal, modulus, psi)) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Checklist", + """ + If a direct transform result looks suspicious, inspect: + + 1. the chosen modulus + 2. the order of the root + 3. whether you are using `ω` or `ψ` + 4. whether the inverse includes `n^-1` + """, + ), + code( + "mandatory", + 2, + "exercise", + "See A Wrong-Root Failure", + """ + from ntt_learning.toy_ntt import find_primitive_root, find_psi, forward_ntt_psi + + signal = [1, 2, 3, 4] + modulus = 17 + omega = find_primitive_root(4, modulus) + psi = find_psi(4, modulus) + + print("correct psi-based transform:", forward_ntt_psi(signal, modulus, psi)) + print("wrongly using omega as if it were psi:", forward_ntt_psi(signal, modulus, omega)) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Explain why “pick any root of unity” is not an acceptable habit in this subject. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Matrix Comparison", + """ + from ntt_learning.toy_ntt import find_primitive_root, find_psi, ntt_psi_matrix + + modulus = 17 + omega = find_primitive_root(4, modulus) + psi = find_psi(4, modulus) + + print("omega:", omega) + print("psi:", psi) + for row in ntt_psi_matrix(4, modulus, psi): + print(row) + """, + ), + handoff_cell("../../../butterfly_mechanics/03_fast_forward_ct/lecture.ipynb"), + ], + ) + + +def build_bundle_03() -> None: + bundle_dir = BUNDLE_DIRS[2] + write_bundle( + bundle_dir, + "Fast Forward CT", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This bundle is where the transform stops being a full matrix multiplication and becomes a staged butterfly network. + + Focus: + + - CT as the fast forward NTT strategy + - visible stage arrays + - explicit zeta values per pair + - BO output vs NO output + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "CT Is A Schedule For Reusing Work", + """ + The point of the CT butterfly is not to invent a new transform. + The point is to compute the same transform by reusing shared bracket terms instead of recomputing everything from scratch. + """, + ), + code( + "mandatory", + 3, + "demo", + "Trace The Exact n=4 Paper Example", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, forward_ntt_psi + from ntt_learning.visuals import interactive_trace, plot_trace_overview + + signal = [1, 2, 3, 4] + modulus = 7681 + psi = 1925 + trace = fast_ntt_psi_ct_trace(signal, modulus, psi) + + print("raw CT output (BO):", trace.raw_output) + print("bit-reversed back to NO:", trace.normal_order_output) + print("direct NTT_psi:", forward_ntt_psi(signal, modulus, psi)) + display(plot_trace_overview(trace, title="CT overview for [1,2,3,4]")) + display(interactive_trace(trace, title="CT forward trace")) + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "What You Should Notice In The Stage Viewer", + """ + Do not just read the final answer. + Notice: + + - which pairs talk to each other in each stage + - which `zeta` each pair uses + - how the array order changes before the final bit-reversal correction + """, + ), + code( + "mandatory", + 3, + "demo", + "Run The Second n=4 Paper Example", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace + from ntt_learning.visuals import interactive_trace + + signal = [5, 6, 7, 8] + trace = fast_ntt_psi_ct_trace(signal, 7681, 1925) + print("BO output:", trace.raw_output) + print("NO output:", trace.normal_order_output) + display(interactive_trace(trace, title="Second CT trace")) + """, + ), + code( + "mandatory", + 3, + "demo", + "Go One Stage Deeper With n=8", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi + from ntt_learning.visuals import interactive_trace, plot_trace_overview + + signal = [0, 1, 2, 3, 4, 5, 6, 7] + modulus = 97 + psi = find_psi(8, modulus) + trace = fast_ntt_psi_ct_trace(signal, modulus, psi) + + print("psi:", psi) + print("BO output:", trace.raw_output) + print("NO output:", trace.normal_order_output) + display(plot_trace_overview(trace, title="Three CT stages for n=8")) + display(interactive_trace(trace, title="n=8 CT stage explorer")) + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + 1. What does CT change: the transform definition or the computation schedule? + 2. Why is the output naturally in BO rather than NO? + 3. In a stage diagram, what are the first three things you should inspect before any formula? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Inspect Stage Rows As Data", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, stage_rows + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + for stage in trace.stages: + print("stage", stage.stage_index) + for row in stage_rows(stage): print(row) - """, - ), - markdown( - "meta", - 1, - "handoff", - "Next Notebook", - """ - Next notebook: `problems.ipynb` - """, - ), - ], - ), + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + You should predict stage pairings and zetas before running the stage explorer. + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 1", + """ + For the `n = 4` example, predict: + + - which original coefficients pair in stage 1 + - which adjacent positions pair in stage 2 + - why the output is not yet in normal order + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check For n=4", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace + from ntt_learning.visuals import interactive_trace + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + display(interactive_trace(trace, title="Check your n=4 prediction")) + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 2", + """ + For the `n = 8` example, name the stage count before you run the next cell. + Then name which stage feels most confusing and why. + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check For n=8", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi + from ntt_learning.visuals import interactive_trace + + trace = fast_ntt_psi_ct_trace([0, 1, 2, 3, 4, 5, 6, 7], 97, find_psi(8, 97)) + display(interactive_trace(trace, title="Check your n=8 prediction")) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - Which stage feels easiest to see by eye? + - Which stage feels most like “pure schedule” rather than “new algebra”? + - Why is BO output not a bug? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Change The n=8 Signal", + """ + import ipywidgets as widgets + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi + from ntt_learning.visuals import interactive_trace + + psi = find_psi(8, 97) + + def preview(a0=0, a1=1, a2=2, a3=3, a4=4, a5=5, a6=6, a7=7): + trace = fast_ntt_psi_ct_trace([a0, a1, a2, a3, a4, a5, a6, a7], 97, psi) + display(interactive_trace(trace, title="Interactive n=8 CT trace")) + + display( + widgets.interact( + preview, + a0=(0, 12), + a1=(0, 12), + a2=(0, 12), + a3=(0, 12), + a4=(0, 12), + a5=(0, 12), + a6=(0, 12), + a7=(0, 12), + ) + ) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the CT schedule is now a visible object rather than a name. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: + + 1. CT makes the transform faster by: + A. changing the ring + B. reusing bracket structure through staged butterflies + C. deleting the inverse + + 2. The natural output order of the direct CT schedule discussed here is: + A. normal order + B. bit-reversed order + C. random order + + 3. A stage explorer is useful mainly because it: + A. hides the pairings + B. makes data movement and zeta usage inspectable + C. removes the need for examples + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "B", 3: "B"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Paper Example Check", + """ + Verify that the CT trace on `[1,2,3,4]` in `Z_7681` with `ψ = 1925` lands on the paper’s BO output. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Check The BO Output", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + print(trace.raw_output) + assert list(trace.raw_output) == [1467, 3471, 2807, 7621] + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + Explain why the phrase “same transform, better schedule” is the right headline for CT. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, find_psi + + trace = fast_ntt_psi_ct_trace([3, 1, 4, 1, 5, 9, 2, 6], 97, find_psi(8, 97)) + for stage in trace.stages: + print(stage.stage_index, stage.output_values) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + This studio compares CT traces and inspects where learners usually lose the plot: stage order, pair order, and BO output. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Same Schedule, Different Signals", + """ + The butterfly pattern is structural. + The signal values change, but the pairing pattern and the zeta schedule stay tied to `n`, the modulus, and the chosen root. + """, + ), + code( + "mandatory", + 3, + "demo", + "Compare Two CT Traces", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace + from ntt_learning.visuals import plot_trace_overview + + trace_a = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + trace_b = fast_ntt_psi_ct_trace([5, 6, 7, 8], 7681, 1925) + + display(plot_trace_overview(trace_a, title="CT trace A")) + display(plot_trace_overview(trace_b, title="CT trace B")) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Checklist", + """ + If a CT implementation looks wrong, inspect: + + 1. the chosen `ψ` + 2. the stage pairings + 3. the zeta exponent sequence + 4. whether you remembered the final BO -> NO reorder when comparing against the direct transform + """, + ), + code( + "mandatory", + 2, + "exercise", + "See A Wrong-Order Comparison Failure", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, forward_ntt_psi + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + direct = forward_ntt_psi([1, 2, 3, 4], 7681, 1925) + + print("wrong comparison: CT BO output vs direct NO output") + print(trace.raw_output, direct) + print("correct comparison: CT NO output vs direct NO output") + print(trace.normal_order_output, direct) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + In one paragraph, explain why a learner can understand the direct transform and still get lost in CT if the stage order and output order are not shown visually. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Inspect The zeta schedule", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + for stage in trace.stages: + print("stage", stage.stage_index, "zetas:", stage.zetas) + """, + ), + handoff_cell("../../../butterfly_mechanics/04_fast_inverse_gs/lecture.ipynb"), + ], ) - write_notebook( - "notebooks/foundations/01_convolution_to_toy_ntt/problems.ipynb", - notebook( - "Problems: Convolution To Toy NTT", - [ - markdown( - "meta", - 1, - "orientation", - "Problem Set Goals", - """ - Use this notebook to check retrieval, not to discover the topic for the first time. - If the questions feel opaque, return to the lecture and lab first. - """, - ), - markdown( - "mandatory", - 2, - "quiz", - "Multiple-Choice Retrieval", - """ - Choose one answer for each: - 1. Negacyclic reduction mainly changes: - A. coefficient labels only - B. wraparound terms by folding them back with sign changes - C. the modulus but not the polynomial ring +def build_bundle_04() -> None: + bundle_dir = BUNDLE_DIRS[3] + write_bundle( + bundle_dir, + "Fast Inverse GS", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This bundle makes the inverse flow explicit. - 2. The toy NTT is introduced early because it: - A. already matches Kyber implementation details exactly - B. removes the need to inspect arrays - C. gives a small, reversible transform that can be inspected directly + Focus: - 3. A butterfly stage is best thought of as: - A. a local rewrite over paired entries - B. a proof that convolution is impossible - C. a random permutation with no arithmetic structure - """, - ), - code( - "mandatory", - 2, - "quiz", - "Answer Key", - """ - answers = { - 1: "B", - 2: "C", - 3: "A", - } + - GS as the fast inverse schedule + - BO input and NO output + - why the final `n^-1` scaling appears + - how bit-reversal fits the forward/inverse pair + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "GS Feels Like The Same Network Seen From The Other End", + """ + The inverse is not “mysterious undoing”. + It is a staged network with the same family resemblance as CT, but with a different direction of arithmetic and a final scaling. + """, + ), + code( + "mandatory", + 3, + "demo", + "Trace The Exact n=4 GS Paper Example", + """ + from IPython.display import display - print(answers) - """, - ), - markdown( - "mandatory", - 2, - "reflection", - "Written Reflection", - """ - Reflection prompt: + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + from ntt_learning.visuals import interactive_trace, plot_trace_overview - - In one paragraph, separate the algebraic purpose of the NTT from the local butterfly dataflow. - - In one sentence, explain why the course postpones Kyber-specific indexing. - """, - ), - code( - "mandatory", - 2, - "exercise", - "Verify A Round Trip", - """ - from ntt_learning.toy_ntt import find_primitive_root, forward_ntt, inverse_ntt + bo_input = [1467, 3471, 2807, 7621] + trace = fast_intt_psi_gs_trace(bo_input, 7681, 1925) - signal = [3, 1, 4, 1] - modulus = 17 - omega = find_primitive_root(order=4, modulus=modulus) - recovered = inverse_ntt(forward_ntt(signal, modulus=modulus, omega=omega), modulus=modulus, omega=omega) + print("unscaled NO output:", trace.raw_output) + print("scaled NO output:", trace.scaled_output) + display(plot_trace_overview(trace, title="GS overview for the n=4 paper example")) + display(interactive_trace(trace, title="GS inverse trace")) + """, + ), + code( + "mandatory", + 3, + "demo", + "See The Bit-Reversal Map Explicitly", + """ + from IPython.display import display - assert recovered == signal - print("round-trip verified:", recovered) - """, - ), - markdown( - "facultative", - 4, - "exploration", - "Optional Challenge", - """ - Try replacing the signal with your own four coefficients and predict the forward spectrum before running the next cell. - """, - ), - code( - "facultative", - 4, - "exploration", - "Explore Another Signal", - """ - from ntt_learning.toy_ntt import find_primitive_root, forward_ntt + from ntt_learning.visuals import plot_bit_reversal_mapping - signal = [6, 0, 5, 2] - modulus = 17 - omega = find_primitive_root(order=4, modulus=modulus) + display(plot_bit_reversal_mapping(4, title="Bit-reversal for n=4")) + display(plot_bit_reversal_mapping(8, title="Bit-reversal for n=8")) + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Why Scaling Waits Until The End", + """ + Each GS stage avoids local division by `2`. + The accumulated effect of those missing local divisions is corrected by the final multiplication with `n^-1`. + """, + ), + code( + "mandatory", + 3, + "demo", + "Full Forward And Inverse Round Trip", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace - print("spectrum:", forward_ntt(signal, modulus=modulus, omega=omega)) - """, - ), - markdown( - "meta", - 1, - "handoff", - "Next Notebook", - """ - Next notebook: `studio.ipynb` - """, - ), - ], - ), + signal = [1, 2, 3, 4] + forward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925) + + print("forward BO output:", forward_trace.raw_output) + print("inverse scaled output:", inverse_trace.scaled_output) + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + 1. Why does GS want BO input? + 2. Why does the final scaling not disappear? + 3. What would go wrong if you visually compared GS input and CT NO output without respecting the order change? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: A Second GS Example", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + from ntt_learning.visuals import interactive_trace + + trace = fast_intt_psi_gs_trace([2489, 6478, 7489, 6607], 7681, 1925) + print("scaled output:", trace.scaled_output) + display(interactive_trace(trace, title="Second GS trace")) + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + Predict the BO input and the final scaling before you run the stage explorer. + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 1", + """ + Explain before running the next cell: + + - why the GS input must be BO in the standard schedule + - why the unscaled output is not yet the original signal + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check", + """ + from IPython.display import display + + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + from ntt_learning.visuals import interactive_trace + + trace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925) + display(interactive_trace(trace, title="Check your GS prediction")) + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 2", + """ + Predict the bit-reversal of `[0,1,2,3,4,5,6,7]` before running the next cell. + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check For Ordering", + """ + from ntt_learning.toy_ntt import bit_reversed_order + + print(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7])) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - Which part of GS felt like a true inverse to you? + - Which part felt like pure bookkeeping? + - Why is the ordering story impossible to ignore? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Explore Another BO Input", + """ + import ipywidgets as widgets + from IPython.display import display + + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + from ntt_learning.visuals import interactive_trace + + def preview(x0=1467, x1=3471, x2=2807, x3=7621): + trace = fast_intt_psi_gs_trace([x0, x1, x2, x3], 7681, 1925) + display(interactive_trace(trace, title="Interactive GS trace")) + + display( + widgets.interact( + preview, + x0=(0, 7680), + x1=(0, 7680), + x2=(0, 7680), + x3=(0, 7680), + ) + ) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the inverse flow, ordering, and scaling are now mechanically stable. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: + + 1. GS is used here as the fast schedule for: + A. direct forward NTT + B. inverse NTT + C. schoolbook multiplication + + 2. The final `n^-1` matters because: + A. each stage skipped local divisions that accumulate + B. the modulus changed mid-computation + C. bit-reversal requires scaling + + 3. In the standard pairing, GS expects: + A. NO input and BO output + B. BO input and NO output + C. random order on both ends + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "A", 3: "B"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Paper Example Check", + """ + Verify that the GS trace on the paper’s BO input scales back to `[1,2,3,4]`. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Check The Final Scaling", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + + trace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925) + print(trace.scaled_output) + assert list(trace.scaled_output) == [1, 2, 3, 4] + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + In one paragraph, explain why “same structure, opposite direction” is a better intuition for GS than “totally different algorithm”. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + + trace = fast_intt_psi_gs_trace([2489, 6478, 7489, 6607], 7681, 1925) + for stage in trace.stages: + print(stage.stage_index, stage.output_values) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + The studio puts CT and GS next to each other and treats ordering as a first-class object, not a side note. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Forward And Inverse Need To Meet In The Middle Cleanly", + """ + The whole point of the pair is: + + - CT gets you into the transform domain efficiently + - GS gets you back out efficiently + - the two only meet cleanly if you respect BO/NO and the final scaling + """, + ), + code( + "mandatory", + 3, + "demo", + "See CT Output Feed GS Input", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace + + signal = [5, 6, 7, 8] + forward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925) + + print("CT BO output:", forward_trace.raw_output) + print("GS scaled output:", inverse_trace.scaled_output) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Checklist", + """ + If the inverse output is wrong, inspect: + + 1. whether the input was BO + 2. whether the zetas were inverse-stage zetas + 3. whether the final `n^-1` scaling was applied + 4. whether you compared the correct order against the direct reference + """, + ), + code( + "mandatory", + 2, + "exercise", + "See A Missing-Scale Failure", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace + + trace = fast_intt_psi_gs_trace([1467, 3471, 2807, 7621], 7681, 1925) + print("unscaled:", trace.raw_output) + print("scaled:", trace.scaled_output) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Explain why “the inverse looked almost right” is a dangerous debugging sentence unless you say what happened with ordering and scaling. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Another bit-reversal map", + """ + from IPython.display import display + + from ntt_learning.visuals import plot_bit_reversal_mapping + + display(plot_bit_reversal_mapping(16, title="Bit-reversal for n=16")) + """, + ), + handoff_cell("../../../kyber_mapping/05_kyber_ntt_and_base_multiplication/lecture.ipynb"), + ], ) - write_notebook( - "notebooks/foundations/01_convolution_to_toy_ntt/studio.ipynb", - notebook( - "Studio: Convolution To Toy NTT", - [ - markdown( - "meta", - 1, - "orientation", - "Studio Goals", - """ - This studio frames implementation reading. - Keep three lenses separate: +def build_bundle_05() -> None: + bundle_dir = BUNDLE_DIRS[4] + write_bundle( + bundle_dir, + "Kyber NTT And Base Multiplication", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This bundle ties the transform story to Kyber without throwing away the concrete arithmetic. - - algebraic purpose - - array dataflow - - protocol-specific conventions - """, - ), - markdown( - "mandatory", - 3, - "explanation", - "Forward And Inverse Flow Side By Side", - """ - The goal here is not to claim the same cell-by-cell formula for both directions. - The goal is to compare the same pairing structure while noticing that forward and inverse flows push arithmetic in opposite directions. - """, - ), - code( - "mandatory", - 3, - "demo", - "Compare Cooley-Tukey And Gentleman-Sande Views", - """ - from ntt_learning.toy_ntt import action_rows, apply_ct_stage, apply_gs_stage + Focus: - values = [2, 5, 7, 1, 3, 6, 4, 0] - ct_output, ct_actions = apply_ct_stage(values, block_size=4, zetas=[1, 4, 1, 4], modulus=17) - gs_output, gs_actions = apply_gs_stage(values, block_size=4, zetas=[1, 4, 1, 4], modulus=17) + - what Kyber’s `q = 3329`, `n = 256` really allow + - why the full `2n`-th root mental model breaks at Kyber v3 + - why base multiplication appears + - how to keep the story NTT-centered without lying about the modulus + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Kyber Is Not “Just Generic Negacyclic NTT With Big Numbers”", + """ + The key arithmetic reality is: - print("input:", values) - print("ct output:", ct_output) - print("gs output:", gs_output) - print("ct trace:", action_rows(ct_actions)) - print("gs trace:", action_rows(gs_actions)) - """, - ), - markdown( - "mandatory", - 2, - "exercise", - "Debug Checklist", - """ - When a stage looks wrong, inspect these in order: + - Kyber v3 has `n = 256` + - `q = 3329` + - `256` divides `3328` + - `512` does **not** divide `3328` - 1. wrong pairings - 2. wrong zeta value - 3. wrong sign in the subtraction branch - 4. wrong direction choice between forward-style and inverse-style flow - """, - ), - code( - "mandatory", - 2, - "exercise", - "Compare Baseline And Wrong-Zeta Output", - """ - from ntt_learning.toy_ntt import apply_ct_stage + That means a primitive `256`-th root exists, but a primitive `512`-th root does not. + So the clean full-length `ψ` story from the toy negative-wrapped transform does not lift over unchanged. + """, + ), + code( + "mandatory", + 3, + "demo", + "Check The Kyber Root Reality Directly", + """ + from ntt_learning.toy_ntt import find_primitive_root - values = [3, 1, 4, 1] - baseline, _ = apply_ct_stage(values, block_size=2, zetas=1, modulus=17) - wrong_zeta, _ = apply_ct_stage(values, block_size=2, zetas=3, modulus=17) + print("3329 - 1 =", 3329 - 1) + print("primitive 256-th root in Z_3329:", find_primitive_root(256, 3329)) + try: + find_primitive_root(512, 3329) + except Exception as exc: + print("512-th root fails exactly because:", exc) + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Tiny Analogue Of The Same Obstruction", + """ + The easiest way to feel this is to shrink the numbers. + In `Z_13`, a 4-th root exists because `4 | 12`, but an 8-th root does not because `8` does not divide `12`. - print("baseline:", baseline) - print("wrong zeta:", wrong_zeta) - """, - ), - markdown( - "facultative", - 4, - "exploration", - "Optional Ordering Preview", - """ - Bit-reversal is important later, but it is deliberately optional here so the main route can stay focused on pair mechanics first. - """, - ), - code( - "facultative", - 4, - "exploration", - "Inspect Bit-Reversed Order", - """ - from ntt_learning.toy_ntt import bit_reversed_order + That is the same shape of obstruction as Kyber v3, only tiny enough to inspect instantly. + """, + ), + code( + "mandatory", + 3, + "demo", + "Run The Tiny Analogue", + """ + from ntt_learning.toy_ntt import find_primitive_root - print(bit_reversed_order([0, 1, 2, 3, 4, 5, 6, 7])) - """, - ), - markdown( - "meta", - 1, - "handoff", - "Next Notebook", - """ - Next notebook: return to `../../COURSE_BLUEPRINT.ipynb` and extend the course into Kyber-specific notebooks. - """, - ), - ], - ), + print("primitive 4-th root in Z_13:", find_primitive_root(4, 13)) + try: + find_primitive_root(8, 13) + except Exception as exc: + print("8-th root fails:", exc) + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Why Base Multiplication Appears", + """ + Once the ring does not split into fully scalar transform slots in the naive `ψ` way, multiplication in the transform domain is no longer “just multiply scalars slot by slot”. + + A small block structure remains, and that is why pairwise base multiplication appears. + """, + ), + code( + "mandatory", + 3, + "demo", + "See A Toy Base Multiplication", + """ + from ntt_learning.toy_ntt import base_multiply_pair + + left = [7, 11] + right = [5, 13] + zeta = 4 + modulus = 17 + + raw = [left[0] * right[0], left[0] * right[1] + left[1] * right[0], left[1] * right[1]] + reduced = [(raw[0] + zeta * raw[2]) % modulus, raw[1] % modulus] + + print("raw degree-2 product:", raw) + print("reduce with x^2 = zeta:", reduced) + print("base_multiply_pair:", base_multiply_pair(left, right, zeta, modulus)) + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + 1. What exact divisibility fact blocks the naive full `2n`-th root story in Kyber v3? + 2. Why does that obstruction point you toward base multiplication? + 3. Why would it be misleading to teach Kyber as if the toy `ψ` story carried over unchanged? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Compare Several Moduli", + """ + def status(n, q): + pwc = (q - 1) % n == 0 + nwc = (q - 1) % (2 * n) == 0 + return {"n": n, "q": q, "pwc": pwc, "nwc": nwc} + + for sample in [(4, 17), (4, 13), (256, 7681), (256, 3329)]: + print(status(*sample)) + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + The lab is about making the Kyber modulus obstruction explicit enough that it becomes memorable. + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 1", + """ + Before running the next cell, say out loud: + + - why `256 | 3328` + - why `512` does not divide `3328` + - what that means for the existence of `ψ` + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check", + """ + print("3328 / 256 =", (3329 - 1) // 256) + print("3328 % 256 =", (3329 - 1) % 256) + print("3328 % 512 =", (3329 - 1) % 512) + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 2", + """ + Verify the toy base multiplication by hand before running the next cell. + """, + ), + code( + "mandatory", + 3, + "exercise", + "Check The Toy Base Multiplication", + """ + from ntt_learning.toy_ntt import base_multiply_pair + + print(base_multiply_pair([3, 5], [2, 7], zeta=6, modulus=17)) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - Why is “there is no 512-th root” not just a technical footnote? + - What false picture of Kyber would survive if you ignored that fact? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Tiny Modulus Classifier", + """ + import ipywidgets as widgets + from IPython.display import display + + def classify(q=17, n=4): + print({"q": q, "n": n, "pwc": (q - 1) % n == 0, "nwc": (q - 1) % (2 * n) == 0}) + + display(widgets.interact(classify, q=(5, 101), n=(2, 16))) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the Kyber-specific modulus story is now precise instead of fuzzy. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: + + 1. For Kyber v3, the important root fact is: + A. `512` divides `3328` + B. `256` divides `3328` but `512` does not + C. no relevant root exists at all + + 2. Base multiplication appears because: + A. the transform-domain multiplication remains structured in small blocks + B. scalar multiplication is forbidden in finite fields + C. schoolbook multiplication vanished + + 3. The biggest pedagogical risk is: + A. teaching Kyber as if the toy `ψ` model applied unchanged + B. teaching any toy examples at all + C. comparing moduli + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "A", 3: "A"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Quick Check", + """ + Explain in one sentence why a primitive 256-th root is not enough to recover the full toy `ψ` story at `n = 256`. + """, + ), + code( + "mandatory", + 2, + "exercise", + "Numerical Check", + """ + print("2 * 256 =", 2 * 256) + print("3329 - 1 =", 3329 - 1) + print("divides?", (3329 - 1) % (2 * 256) == 0) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + In one paragraph, explain why the Kyber notebook belongs after the toy direct and fast-transform notebooks rather than before them. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + samples = [(256, 7681), (256, 3329), (8, 97), (8, 41)] + for n, q in samples: + print({"n": n, "q": q, "n_divides_q_minus_1": (q - 1) % n == 0, "two_n_divides_q_minus_1": (q - 1) % (2 * n) == 0}) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + This studio compares the clean toy story with the Kyber-specific modulus reality and treats the mismatch as the lesson. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "The Mismatch Is Not A Bug In The Course", + """ + The mismatch between “toy full `ψ` story” and “Kyber v3 modulus reality” is exactly what the learner needs to understand. + + That mismatch is why base multiplication and implementation-specific scheduling matter. + """, + ), + code( + "mandatory", + 3, + "demo", + "Toy Full ψ Story vs Kyber Root Reality", + """ + from ntt_learning.toy_ntt import find_psi + + print("toy n=4, q=17 has psi:", find_psi(4, 17)) + try: + find_psi(256, 3329) + except Exception as exc: + print("Kyber v3 does not have that full psi story:", exc) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Checklist", + """ + If somebody says “Kyber is just the same toy negative-wrapped NTT with bigger numbers”, inspect: + + 1. whether they checked `2n | q - 1` + 2. whether they accounted for the missing `ψ` + 3. whether they know why base multiplication appears + """, + ), + code( + "mandatory", + 2, + "exercise", + "See The Exact Obstruction Again", + """ + q = 3329 + n = 256 + print({"q_minus_1": q - 1, "n": n, "2n": 2 * n, "q_minus_1_mod_n": (q - 1) % n, "q_minus_1_mod_2n": (q - 1) % (2 * n)}) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Explain why the Kyber base-multiplication story is easier to trust once you have already internalized the toy negacyclic transform. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Another toy base multiplication", + """ + from ntt_learning.toy_ntt import base_multiply_pair + + print(base_multiply_pair([7, 9], [4, 6], zeta=11, modulus=17)) + """, + ), + handoff_cell("../../../professional/06_debugging_ntt_failures/lecture.ipynb"), + ], ) +def build_bundle_06() -> None: + bundle_dir = BUNDLE_DIRS[5] + write_bundle( + bundle_dir, + "Debugging NTT Failures", + lecture=[ + markdown( + "meta", + 1, + "orientation", + "Objectives", + """ + This final bundle turns common failure modes into visible patterns instead of vague warnings. + + Focus: + + - wrong sign in wraparound + - wrong root or wrong zeta + - wrong BO / NO comparison + - missing final scaling + - wrong mental model for the Kyber modulus + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "Bad Outputs Have Fingerprints", + """ + Debugging NTTs is easier when you stop staring at the final vector as one blob. + Each common mistake leaves a characteristic fingerprint: + + - wrong sign flips specific wrapped slots + - wrong order makes a correct value set appear shuffled + - missing `n^-1` keeps the shape but scales everything wrong + - wrong zeta corrupts local pair structure early + """, + ), + code( + "mandatory", + 3, + "demo", + "See Four Failure Modes Side By Side", + """ + from ntt_learning.toy_ntt import ( + fast_intt_psi_gs_trace, + fast_ntt_psi_ct_trace, + forward_ntt_psi, + negacyclic_reduce, + schoolbook_convolution, + ) + + signal = [1, 2, 3, 4] + forward_trace = fast_ntt_psi_ct_trace(signal, 7681, 1925) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925) + + raw = schoolbook_convolution([1, 2, 3, 4], [5, 6, 7, 8]) + wrong_sign = [raw[0] + raw[4], raw[1] + raw[5], raw[2] + raw[6], raw[3]] + wrong_order = list(forward_trace.raw_output) + wrong_scale = list(inverse_trace.raw_output) + wrong_root = forward_ntt_psi(signal, 7681, 3383) + + print("wrong sign fold:", wrong_sign) + print("correct sign fold:", negacyclic_reduce(raw, n=4)) + print("wrong BO-vs-NO comparison:", wrong_order) + print("correct NO output:", forward_trace.normal_order_output) + print("missing final scaling:", wrong_scale) + print("wrong root in direct transform:", wrong_root) + """, + ), + code( + "mandatory", + 3, + "demo", + "Interactive Failure Picker", + """ + import ipywidgets as widgets + from IPython.display import display + + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace, forward_ntt_psi + + forward_trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925) + + failures = { + "wrong_order": list(forward_trace.raw_output), + "correct_order": list(forward_trace.normal_order_output), + "missing_scale": list(inverse_trace.raw_output), + "scaled": list(inverse_trace.scaled_output), + "wrong_root": forward_ntt_psi([1, 2, 3, 4], 7681, 3383), + } + + def preview(mode="wrong_order"): + print(mode, "->", failures[mode]) + + display(widgets.interact(preview, mode=sorted(failures))) + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Retrieval Check", + """ + 1. Which mistake keeps the general shape of the inverse output but leaves every entry too large by a shared factor? + 2. Which mistake often disappears once you apply the correct BO -> NO reorder? + 3. Which mistake shows up earliest in local pair traces rather than only at the very end? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Trace Rows For Debugging", + """ + from ntt_learning.toy_ntt import fast_ntt_psi_ct_trace, stage_rows + + trace = fast_ntt_psi_ct_trace([1, 2, 3, 4], 7681, 1925) + for stage in trace.stages: + print("stage", stage.stage_index) + for row in stage_rows(stage): + print(row) + """, + ), + handoff_cell("lab.ipynb"), + ], + lab=[ + markdown( + "meta", + 1, + "orientation", + "Lab Goals", + """ + The lab asks you to match output fingerprints to the underlying bug. + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 1", + """ + Before running the next cell, predict which of these bug labels goes with each fingerprint: + + - shuffled but otherwise familiar values + - values that look uniformly too large + - values broken already in a local stage pair + """, + ), + code( + "mandatory", + 3, + "exercise", + "Prediction Check", + """ + fingerprints = { + "wrong_order": "shuffled but same value set", + "missing_scale": "same shape but uniformly off by a factor", + "wrong_zeta": "local pair outputs go bad immediately", + } + print(fingerprints) + """, + ), + markdown( + "mandatory", + 3, + "exercise", + "Exercise 2", + """ + Explain why “almost right” is a useless debugging description unless you also specify whether the issue is: + + - sign + - order + - zeta + - scaling + """, + ), + code( + "mandatory", + 3, + "exercise", + "A Small Debugging Drill", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace + + forward_trace = fast_ntt_psi_ct_trace([5, 6, 7, 8], 7681, 1925) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 7681, 1925) + + print("forward BO output:", forward_trace.raw_output) + print("forward NO output:", forward_trace.normal_order_output) + print("inverse unscaled:", inverse_trace.raw_output) + print("inverse scaled:", inverse_trace.scaled_output) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Reflection", + """ + Reflection prompt: + + - Which bug fingerprint feels easiest to recognize now? + - Which one still needs more repetition? + - What is your debugging order of operations now? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Build Your Own Fingerprint Table", + """ + import ipywidgets as widgets + from IPython.display import display + + def note_bug(mode="wrong_order"): + print({"mode": mode, "what_to_check_first": {"wrong_order": "bit reversal", "missing_scale": "n^-1", "wrong_zeta": "local pair twiddle", "wrong_sign": "negacyclic fold sign"}[mode]}) + + display(widgets.interact(note_bug, mode=["wrong_order", "missing_scale", "wrong_zeta", "wrong_sign"])) + """, + ), + handoff_cell("problems.ipynb"), + ], + problems=[ + markdown( + "meta", + 1, + "orientation", + "Problem Set Goals", + """ + This notebook checks whether the main NTT bug classes are now distinct in memory. + """, + ), + markdown( + "mandatory", + 2, + "quiz", + "Multiple-Choice Retrieval", + """ + Choose one answer for each: + + 1. A shuffled but otherwise familiar forward result most strongly suggests: + A. missing final scaling + B. wrong BO / NO comparison + C. wrong modulus + + 2. An inverse output that looks like a clean multiple of the target most strongly suggests: + A. missing `n^-1` + B. wrong wraparound sign + C. wrong bit-reversal map + + 3. A local pair that already looks broken in stage 1 most strongly suggests: + A. wrong zeta + B. correct CT output + C. harmless ordering noise + """, + ), + code( + "mandatory", + 2, + "quiz", + "Answer Key", + """ + answers = {1: "B", 2: "A", 3: "A"} + print(answers) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Debug Priority Check", + """ + List the first four checks you would run on a suspicious iNTT output. + """, + ), + code( + "mandatory", + 2, + "exercise", + "One Good Ordering", + """ + debug_order = [ + "check BO / NO assumption", + "check root / zeta schedule", + "check final n^-1 scaling", + "check sign / wraparound conventions", + ] + print(debug_order) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Written Reflection", + """ + In one paragraph, explain why visible traces are much better debugging tools than only comparing final vectors. + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional Challenge", + """ + mistakes = { + "wrong_order": "fix the permutation first", + "missing_scale": "multiply by n^-1", + "wrong_zeta": "rebuild the twiddle schedule", + "wrong_sign": "inspect the quotient ring", + } + for key, value in mistakes.items(): + print(key, "->", value) + """, + ), + handoff_cell("studio.ipynb"), + ], + studio=[ + markdown( + "meta", + 1, + "orientation", + "Studio Goals", + """ + The last studio compresses the whole course into one debugging mindset. + """, + ), + markdown( + "mandatory", + 3, + "explanation", + "The Whole Course Is A Debugging Ladder", + """ + Every earlier bundle built one layer of the debugging stack: + + - schoolbook grid and wraparound + - direct transform algebra + - CT stage schedule + - GS inverse schedule + - bit-reversal and scaling + - Kyber modulus constraints and base multiplication + """, + ), + code( + "mandatory", + 3, + "demo", + "Print The Whole Debugging Ladder", + """ + ladder = [ + "Can I see the raw schoolbook product grid?", + "Can I explain the negacyclic wraparound sign?", + "Can I reproduce the direct NTT_psi / INTT_psi round trip?", + "Can I trace CT stages with the right zetas?", + "Can I trace GS stages with the right order and scaling?", + "Can I explain why Kyber v3 does not inherit the full toy psi story unchanged?", + ] + for item in ladder: + print("-", item) + """, + ), + markdown( + "mandatory", + 2, + "exercise", + "Final Debug Checklist", + """ + Keep this order: + + 1. ring rule and wraparound + 2. root existence and root choice + 3. stage pairings and zetas + 4. BO vs NO comparison + 5. final `n^-1` scaling + 6. Kyber-specific modulus / base-multiplication assumptions + """, + ), + code( + "mandatory", + 2, + "exercise", + "One Last Round Trip", + """ + from ntt_learning.toy_ntt import fast_intt_psi_gs_trace, fast_ntt_psi_ct_trace + + signal = [3, 1, 4, 1] + forward_trace = fast_ntt_psi_ct_trace(signal, 17, 2) + inverse_trace = fast_intt_psi_gs_trace(forward_trace.raw_output, 17, 2) + + print("signal:", signal) + print("forward BO:", forward_trace.raw_output) + print("inverse scaled:", inverse_trace.scaled_output) + """, + ), + markdown( + "mandatory", + 2, + "reflection", + "Final Reflection", + """ + Final prompt: + + - Which image now anchors your understanding of NTT best: the schoolbook grid, the fold arrows, the CT stage view, the GS stage view, or the bit-reversal map? + - Why that one? + """, + ), + code( + "facultative", + 4, + "exploration", + "Optional: Personal Debug Rule", + """ + personal_rule = "Never trust a final vector until I have checked the stage trace, the order, and the scaling." + print(personal_rule) + """, + ), + handoff_cell("../../../COURSE_COMPLETE.ipynb"), + ], + ) + + +def build_notebooks() -> None: + build_start_here() + build_course_blueprint() + build_bundle_01() + build_bundle_02() + build_bundle_03() + build_bundle_04() + build_bundle_05() + build_bundle_06() + build_course_complete() + + if __name__ == "__main__": build_notebooks()