Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
duyuefeng0708
GitHub Repository: duyuefeng0708/Cryptography-From-First-Principle
Path: blob/main/shared/convert_sage_to_python.py
483 views
unlisted
1
#!/usr/bin/env python3
2
"""
3
Convert SageMath notebooks to pure Python notebooks using cryptolab.
4
5
Reads .ipynb files from sage/ directories, applies mechanical translations,
6
and writes to python/ directories. Manual review is still needed for
7
visualization-heavy cells.
8
"""
9
10
import json
11
import re
12
import sys
13
import os
14
15
16
# Standard header cell for all Python notebooks
17
HEADER_SOURCE = [
18
"# This notebook has two versions:\n",
19
"# Python (this file) -- runs in browser via JupyterLite, no install needed\n",
20
"# SageMath (../sage/) -- richer algebra system, needs local install or Codespaces\n",
21
"#\n",
22
"# Both versions cover the same material. Choose whichever works for you.\n",
23
"\n",
24
"import sys, os\n",
25
"sys.path.insert(0, os.path.join('..', '..', '..', 'shared'))\n",
26
]
27
28
29
# Import lines added based on what the notebook uses
30
IMPORT_MAP = {
31
'Mod(': 'from cryptolab import Mod',
32
'Zmod(': 'from cryptolab import Zmod',
33
'Integers(': 'from cryptolab import Integers',
34
'gcd(': 'from cryptolab import gcd',
35
'euler_phi(': 'from cryptolab import euler_phi',
36
'factor(': 'from cryptolab import factor',
37
'divisors(': 'from cryptolab import divisors',
38
'is_prime(': 'from cryptolab import is_prime',
39
'power_mod(': 'from cryptolab import power_mod',
40
'inverse_mod(': 'from cryptolab import inverse_mod',
41
'primitive_root(': 'from cryptolab import primitive_root',
42
'discrete_log(': 'from cryptolab import discrete_log',
43
'CRT(': 'from cryptolab import crt',
44
'DiGraph(': 'from cryptolab.plot import cayley_graph',
45
'Poset(': 'from cryptolab.plot import subgroup_lattice',
46
'matrix_plot(': 'from cryptolab.plot import multiplication_heatmap',
47
'Graphics(': 'from cryptolab.plot import cycle_diagram, coset_coloring',
48
'graphics_array(': 'from cryptolab.plot import graphics_array',
49
}
50
51
52
def detect_imports(notebook):
53
"""Scan all code cells to determine which imports are needed."""
54
all_code = ''
55
for cell in notebook['cells']:
56
if cell['cell_type'] == 'code':
57
all_code += ''.join(cell['source'])
58
59
imports = set()
60
for trigger, import_line in IMPORT_MAP.items():
61
if trigger in all_code:
62
imports.add(import_line)
63
64
# Group imports from cryptolab
65
cryptolab_names = []
66
plot_names = []
67
other_imports = []
68
69
for imp in sorted(imports):
70
if imp.startswith('from cryptolab.plot'):
71
names = imp.replace('from cryptolab.plot import ', '').split(', ')
72
plot_names.extend(names)
73
elif imp.startswith('from cryptolab'):
74
names = imp.replace('from cryptolab import ', '').split(', ')
75
cryptolab_names.extend(names)
76
else:
77
other_imports.append(imp)
78
79
lines = []
80
if cryptolab_names:
81
names_str = ', '.join(sorted(set(cryptolab_names)))
82
lines.append(f'from cryptolab import {names_str}\n')
83
if plot_names:
84
names_str = ', '.join(sorted(set(plot_names)))
85
lines.append(f'from cryptolab.plot import {names_str}\n')
86
for imp in other_imports:
87
lines.append(imp + '\n')
88
89
return lines
90
91
92
def translate_code(source_lines):
93
"""Apply mechanical SageMath -> Python translations to code cell source."""
94
result = []
95
for line in source_lines:
96
translated = translate_line(line)
97
result.append(translated)
98
return result
99
100
101
def translate_line(line):
102
"""Translate a single line of SageMath code to Python."""
103
original = line
104
105
# Remove SageMath-specific type conversions
106
line = re.sub(r'\bInteger\(([^)]+)\)', r'int(\1)', line)
107
line = re.sub(r'\bZZ\(([^)]+)\)', r'int(\1)', line)
108
109
# CRT -> crt (lowercase)
110
line = re.sub(r'\bCRT\(', 'crt(', line)
111
112
# ^ -> ** for exponentiation (but not in strings or comments)
113
# This is tricky - only translate ^ that are not in strings/comments
114
line = translate_caret(line)
115
116
# var('t') -> remove (not needed)
117
if re.match(r"\s*var\s*\(\s*['\"]t['\"]\s*\)\s*$", line):
118
return '# (SageMath variable declaration removed)\n'
119
120
# SageMath math functions -> Python math
121
line = re.sub(r'\bcos\(', 'math.cos(', line)
122
line = re.sub(r'\bsin\(', 'math.sin(', line)
123
line = re.sub(r'\bsqrt\(', 'math.sqrt(', line)
124
line = re.sub(r'\babs\(', 'abs(', line)
125
# pi -> math.pi (but not in strings)
126
line = re.sub(r'\bpi\b(?![\'"])', 'math.pi', line)
127
128
# If we added math references, we should note the import is needed
129
# (handled by adding 'import math' in imports)
130
131
return line
132
133
134
def translate_caret(line):
135
"""Replace ^ with ** outside of strings and comments."""
136
# Skip if line is a comment
137
stripped = line.lstrip()
138
if stripped.startswith('#'):
139
return line
140
141
result = []
142
in_string = False
143
string_char = None
144
i = 0
145
while i < len(line):
146
c = line[i]
147
if in_string:
148
result.append(c)
149
if c == '\\' and i + 1 < len(line):
150
result.append(line[i + 1])
151
i += 2
152
continue
153
if c == string_char:
154
in_string = False
155
i += 1
156
else:
157
if c in ('"', "'"):
158
# Check for triple quotes
159
if line[i:i+3] in ('"""', "'''"):
160
result.append(line[i:i+3])
161
in_string = True
162
string_char = line[i:i+3]
163
i += 3
164
continue
165
in_string = True
166
string_char = c
167
result.append(c)
168
i += 1
169
elif c == '#':
170
# Rest of line is comment
171
result.append(line[i:])
172
break
173
elif c == '^':
174
result.append('**')
175
i += 1
176
else:
177
result.append(c)
178
i += 1
179
return ''.join(result)
180
181
182
def needs_math_import(source_lines):
183
"""Check if any line uses math functions."""
184
code = ''.join(source_lines)
185
return any(f in code for f in ['math.cos', 'math.sin', 'math.sqrt', 'math.pi'])
186
187
188
def convert_notebook(input_path, output_path):
189
"""Convert a SageMath notebook to a Python notebook."""
190
with open(input_path, 'r') as f:
191
nb = json.load(f)
192
193
# Detect needed imports
194
import_lines = detect_imports(nb)
195
196
# Check if math is needed after translation
197
all_translated = []
198
for cell in nb['cells']:
199
if cell['cell_type'] == 'code':
200
translated = translate_code(cell['source'])
201
all_translated.extend(translated)
202
203
if needs_math_import(all_translated):
204
import_lines.insert(0, 'import math\n')
205
206
# Build header cell
207
header_cell = {
208
'cell_type': 'code',
209
'execution_count': None,
210
'metadata': {},
211
'outputs': [],
212
'source': HEADER_SOURCE + ['\n'] + import_lines
213
}
214
215
# Convert cells
216
new_cells = [header_cell]
217
for cell in nb['cells']:
218
if cell['cell_type'] == 'markdown':
219
new_cell = {
220
'cell_type': 'markdown',
221
'metadata': {},
222
'source': translate_markdown(cell['source'])
223
}
224
new_cells.append(new_cell)
225
elif cell['cell_type'] == 'code':
226
translated_source = translate_code(cell['source'])
227
new_cell = {
228
'cell_type': 'code',
229
'execution_count': None,
230
'metadata': {},
231
'outputs': [],
232
'source': translated_source
233
}
234
new_cells.append(new_cell)
235
236
# Build output notebook with Python 3 kernel
237
output_nb = {
238
'nbformat': 4,
239
'nbformat_minor': 5,
240
'metadata': {
241
'kernelspec': {
242
'display_name': 'Python 3',
243
'language': 'python',
244
'name': 'python3'
245
},
246
'language_info': {
247
'name': 'python',
248
'version': '3.12.0'
249
}
250
},
251
'cells': new_cells
252
}
253
254
os.makedirs(os.path.dirname(output_path), exist_ok=True)
255
with open(output_path, 'w') as f:
256
json.dump(output_nb, f, indent=1)
257
258
return len(new_cells)
259
260
261
def translate_markdown(source_lines):
262
"""Translate markdown cells. Mostly verbatim but update SageMath references."""
263
result = []
264
for line in source_lines:
265
# Update references to SageMath-specific tools
266
line = line.replace("SageMath's `Mod()`", "`Mod()` from cryptolab")
267
line = line.replace("SageMath's `Zmod(n)`", "`Zmod(n)` from cryptolab")
268
line = line.replace('in SageMath', 'in Python')
269
line = line.replace('SageMath computes this with', 'We can compute this with')
270
line = line.replace('SageMath can also compute', 'We can also compute')
271
line = line.replace('SageMath confirms:', 'Computed:')
272
line = line.replace('Use `divmod()` and `Mod()` in SageMath',
273
'Use `divmod()` and `Mod()` from cryptolab')
274
result.append(line)
275
return result
276
277
278
def main():
279
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
280
mod01 = os.path.join(base, 'foundations', '01-modular-arithmetic-groups')
281
282
conversions = [
283
('sage/01a-integers-and-division.ipynb', 'python/01a-integers-and-division.ipynb'),
284
('sage/01b-modular-arithmetic.ipynb', 'python/01b-modular-arithmetic.ipynb'),
285
('sage/01c-groups-first-look.ipynb', 'python/01c-groups-first-look.ipynb'),
286
('sage/01d-cyclic-groups-generators.ipynb', 'python/01d-cyclic-groups-generators.ipynb'),
287
('sage/01e-subgroups-lagrange.ipynb', 'python/01e-subgroups-lagrange.ipynb'),
288
('sage/01f-group-visualization.ipynb', 'python/01f-group-visualization.ipynb'),
289
('break/weak-generator-attack.ipynb', 'python/break-weak-generator-attack.ipynb'),
290
('break/smooth-order-attack.ipynb', 'python/break-smooth-order-attack.ipynb'),
291
('connect/dh-parameter-selection.ipynb', 'python/connect-dh-parameter-selection.ipynb'),
292
('connect/rsa-key-generation.ipynb', 'python/connect-rsa-key-generation.ipynb'),
293
]
294
295
for sage_rel, python_rel in conversions:
296
sage_path = os.path.join(mod01, sage_rel)
297
python_path = os.path.join(mod01, python_rel)
298
if os.path.exists(sage_path):
299
n_cells = convert_notebook(sage_path, python_path)
300
print(f' {sage_rel} -> {python_rel} ({n_cells} cells)')
301
else:
302
print(f' SKIP {sage_rel} (not found)')
303
304
305
if __name__ == '__main__':
306
main()
307
308