cct-cli 0.7.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,558 @@
1
+ """
2
+ Scientific rendering — matplotlib PNG snapshots styled after real
3
+ quantum-chemistry visualizations (probability-density point clouds,
4
+ Hydrogen-Wave-Function-style orbital grids, and ELF-style chemical
5
+ bonding density maps).
6
+
7
+ Everything here is additive: it does not touch the existing live
8
+ ASCII/ANSI terminal animations in atomsim.py / sim3d.py / gpu3d.py,
9
+ it only gives the "press s to snapshot" PNG export path a much more
10
+ scientific look, and adds two new renders (orbital grid, bonding map)
11
+ that didn't exist before.
12
+
13
+ All renders:
14
+ - pure black background (#000000), no axes/ticks/spines
15
+ - a small monospace caption in the corner (matches the
16
+ "QUANTUM ORBITAL PROBABILITY DENSITY VISUALIZER" style caption)
17
+ - saved to ~/cct_exports and the path is returned (None if
18
+ matplotlib/numpy/scipy aren't installed)
19
+
20
+ Physics notes:
21
+ - Atom / orbital renders use the *real* hydrogen-like radial
22
+ wavefunction (via the generalized Laguerre polynomials) and the
23
+ real spherical-harmonic angular density (via associated Legendre
24
+ functions) — not a cartoon approximation. |psi|^2 is exact for a
25
+ hydrogenic (single-electron) atom in those quantum numbers.
26
+ - The bonding/ELF map is NOT a real Electron Localization Function
27
+ from a DFT calculation (that needs an actual electronic-structure
28
+ solver). It's a stylized analytic stand-in — concentric per-atom
29
+ "shell" rings plus Gaussian bond-lobes, tuned to reproduce the
30
+ *visual language* of a real ELF plot (rings around cores, bright
31
+ bonding saddle regions, rainbow colormap) for illustration inside
32
+ the app. Documented here so nobody mistakes it for lab output.
33
+ """
34
+
35
+ import math
36
+ import os
37
+
38
+ try:
39
+ import numpy as np
40
+ from scipy.special import genlaguerre, lpmv
41
+ import matplotlib
42
+ matplotlib.use("Agg")
43
+ import matplotlib.pyplot as plt
44
+ from matplotlib.colors import LinearSegmentedColormap
45
+ from scipy.ndimage import gaussian_filter
46
+ HAVE_DEPS = True
47
+ except ImportError:
48
+ HAVE_DEPS = False
49
+
50
+ BG = "#000000"
51
+
52
+
53
+ def _export_dir(category="simulations"):
54
+ """Renders go into the shared Project Workspace now (spec section
55
+ 16, calc_terminal/workspace.py) instead of scires.py's own
56
+ ~/cct_exports. Falls back to the old location only if the
57
+ workspace module can't be imported for some reason, so a broken
58
+ workspace config never blocks a render outright.
59
+
60
+ STRICT FIX v0.7.9.6: explicit ensure=True — this is a WRITE path
61
+ (render requested explicitly). Read paths never create dirs."""
62
+ try:
63
+ from . import workspace
64
+ return workspace.category_dir(category, ensure=True)
65
+ except Exception:
66
+ d = os.path.join(os.path.expanduser("~"), "cct_exports")
67
+ os.makedirs(d, exist_ok=True)
68
+ return d
69
+
70
+
71
+ def _caption(ax, text, color="#e8ebfa"):
72
+ ax.text(0.015, 0.02, text, transform=ax.transAxes, color=color,
73
+ fontsize=7.5, family="monospace", alpha=0.85, va="bottom")
74
+
75
+
76
+ def _no_axes(ax):
77
+ ax.set_facecolor(BG)
78
+ ax.set_xticks([])
79
+ ax.set_yticks([])
80
+ for s in ax.spines.values():
81
+ s.set_visible(False)
82
+
83
+
84
+ def _wireframe_box(ax, half, n=2, color="#ffffff", alpha=0.35, lw=0.7, seed=0):
85
+ """Decorative rotated-square wireframe accents (the crossed-diamond
86
+ look behind the point cloud in reference orbital renders)."""
87
+ rng = np.random.default_rng(seed)
88
+ for _ in range(n):
89
+ ang = rng.uniform(0, math.pi)
90
+ pts = []
91
+ for k in range(5):
92
+ a = ang + k * math.pi / 2
93
+ pts.append((half * 1.15 * math.cos(a), half * 1.15 * math.sin(a)))
94
+ xs, ys = zip(*pts)
95
+ ax.plot(xs, ys, color=color, alpha=alpha, lw=lw)
96
+
97
+
98
+ # ----------------------------------------------------------- hydrogen --
99
+ def hydrogen_R(n, l, r):
100
+ """Exact (normalized) hydrogenic radial wavefunction, a0 units, Z=1."""
101
+ r = np.asarray(r, dtype=float)
102
+ rho = 2.0 * r / n
103
+ lag = genlaguerre(n - l - 1, 2 * l + 1)(rho)
104
+ norm = math.sqrt((2.0 / n) ** 3 * math.factorial(n - l - 1) /
105
+ (2.0 * n * math.factorial(n + l)))
106
+ return norm * np.exp(-rho / 2.0) * rho ** l * lag
107
+
108
+
109
+ def hydrogen_angular2(l, m, theta):
110
+ """|Y_l^m(theta,phi)|^2 — independent of phi since the complex
111
+ exponential has unit modulus, so this is exact and axisymmetric."""
112
+ m = abs(m)
113
+ cos_t = np.cos(theta)
114
+ p = lpmv(m, l, cos_t)
115
+ norm = (2 * l + 1) / (4 * math.pi) * math.factorial(l - m) / math.factorial(l + m)
116
+ return norm * p ** 2
117
+
118
+
119
+ def psi2(n, l, m, r, theta):
120
+ """|psi_nlm(r,theta)|^2 at a point in the meridian half-plane."""
121
+ return hydrogen_R(n, l, r) ** 2 * hydrogen_angular2(l, m, theta)
122
+
123
+
124
+ R_MAX_FOR_N = {1: 6, 2: 15, 3: 28, 4: 44}
125
+
126
+
127
+ # --------------------------------------------------- 1. atom / nucleus --
128
+ def render_atom(z, symbol, name, shells, mass, out_name=None):
129
+ """Nucleus (protons/neutrons) + electron shells, glow-density style
130
+ on black, matching the rendering language of the orbital plots."""
131
+ if not HAVE_DEPS:
132
+ return None
133
+ neutrons = mass - z
134
+ rng = np.random.default_rng(z * 7)
135
+
136
+ fig, ax = plt.subplots(figsize=(6.4, 6.4), facecolor=BG)
137
+ _no_axes(ax)
138
+
139
+ max_shell_r = len(shells) + 1.0
140
+
141
+ # faint dotted shell orbits
142
+ th = np.linspace(0, 2 * math.pi, 400)
143
+ for si in range(len(shells)):
144
+ r = si + 1.3
145
+ ax.plot(r * np.cos(th), r * np.sin(th), color="#3b4261", lw=0.6,
146
+ alpha=0.55, ls=(0, (1, 3)))
147
+
148
+ # electrons as soft glow dots at random shell phase (static snapshot)
149
+ for si, count in enumerate(shells):
150
+ r = si + 1.3
151
+ for e in range(count):
152
+ a = 2 * math.pi * e / max(count, 1) + rng.uniform(-0.15, 0.15)
153
+ ex, ey = r * math.cos(a), r * math.sin(a)
154
+ for gs, ga in ((90, 0.05), (35, 0.12), (10, 0.9)):
155
+ ax.scatter([ex], [ey], s=gs, c="#7dcfff", alpha=ga, linewidths=0)
156
+
157
+ # nucleus: protons + neutrons packed, glow-layered
158
+ total = z + neutrons
159
+ pack_r = max(0.5, math.sqrt(total) * 0.24)
160
+ kinds = [True] * z + [False] * neutrons
161
+ rng.shuffle(kinds)
162
+ xs, ys = [], []
163
+ for is_p in kinds[:80]:
164
+ ang = rng.uniform(0, 2 * math.pi)
165
+ rr = pack_r * math.sqrt(rng.uniform(0, 1))
166
+ xs.append(rr * math.cos(ang))
167
+ ys.append(rr * math.sin(ang))
168
+ xs, ys = np.array(xs), np.array(ys)
169
+ is_proton = np.array(kinds[:80])
170
+ for gs, ga in ((260, 0.05), (110, 0.12)):
171
+ ax.scatter(xs[is_proton], ys[is_proton], s=gs, c="#ff7f9a", alpha=ga, linewidths=0)
172
+ ax.scatter(xs[~is_proton], ys[~is_proton], s=gs, c="#8891c9", alpha=ga, linewidths=0)
173
+ ax.scatter(xs[is_proton], ys[is_proton], s=22, c="#ff9fb4", alpha=0.95, linewidths=0)
174
+ ax.scatter(xs[~is_proton], ys[~is_proton], s=22, c="#a9b1e8", alpha=0.95, linewidths=0)
175
+
176
+ ax.set_xlim(-max_shell_r, max_shell_r)
177
+ ax.set_ylim(-max_shell_r, max_shell_r)
178
+ ax.set_aspect("equal")
179
+ ax.set_title(f"{name} ({symbol}) Z={z} A\u2248{mass}", color="#c0caf5",
180
+ fontsize=12, family="monospace", pad=10)
181
+ _caption(ax, f"NUCLEUS: {z} PROTON{'S' if z != 1 else ''} \u00b7 {neutrons} NEUTRON{'S' if neutrons != 1 else ''}"
182
+ f" \u2014 {z} ELECTRON{'S' if z != 1 else ''} ({', '.join(str(c) for c in shells)})")
183
+
184
+ path = os.path.join(_export_dir(), out_name or f"atom_{symbol.lower()}.png")
185
+ fig.savefig(path, dpi=170, facecolor=fig.get_facecolor())
186
+ plt.close(fig)
187
+ return path
188
+
189
+
190
+ # -------------------------------------------------- 2. single orbital --
191
+ CMAP_MAGMA_BLACK = None
192
+
193
+
194
+ def render_orbital(orbital_key, n, l, m, n_points=18000, out_name=None):
195
+ """Point-cloud probability density render, colored by local point
196
+ density (magma), on black — the 'live electron cloud' aesthetic."""
197
+ if not HAVE_DEPS:
198
+ return None
199
+ rng = np.random.default_rng(abs(hash((n, l, m))) % (2 ** 32))
200
+ r_max = R_MAX_FOR_N.get(n, 40)
201
+
202
+ # rejection-sample the true 3D density including the r^2*sin(theta)
203
+ # spherical volume element
204
+ grid_r = np.linspace(1e-4, r_max, 220)
205
+ grid_t = np.linspace(1e-4, math.pi - 1e-4, 90)
206
+ RR, TT = np.meshgrid(grid_r, grid_t, indexing="ij")
207
+ dens = psi2(n, l, m, RR, TT) * RR ** 2 * np.sin(TT)
208
+ peak = dens.max()
209
+
210
+ pts = []
211
+ batch = max(n_points * 6, 20000)
212
+ tries = 0
213
+ while len(pts) < n_points and tries < 40:
214
+ tries += 1
215
+ r = rng.uniform(0, r_max, batch)
216
+ th = rng.uniform(0, math.pi, batch)
217
+ val = psi2(n, l, m, r, th) * r ** 2 * np.sin(th)
218
+ acc = rng.uniform(0, peak, batch) <= val
219
+ r, th = r[acc], th[acc]
220
+ phi = rng.uniform(0, 2 * math.pi, len(r))
221
+ x = r * np.sin(th) * np.cos(phi)
222
+ z_ = r * np.cos(th)
223
+ pts.extend(zip(x.tolist(), z_.tolist()))
224
+ pts = np.array(pts[:n_points]) if pts else np.zeros((0, 2))
225
+
226
+ fig, ax = plt.subplots(figsize=(6.4, 6.4), facecolor=BG)
227
+ _no_axes(ax)
228
+ half = r_max * 0.75
229
+
230
+ if len(pts):
231
+ # local density via a blurred 2D histogram, sampled back at each point
232
+ bins = 160
233
+ H, xedges, yedges = np.histogram2d(pts[:, 0], pts[:, 1], bins=bins,
234
+ range=[[-half, half], [-half, half]])
235
+ H = gaussian_filter(H, sigma=1.4)
236
+ xi = np.clip(np.digitize(pts[:, 0], xedges) - 1, 0, bins - 1)
237
+ yi = np.clip(np.digitize(pts[:, 1], yedges) - 1, 0, bins - 1)
238
+ c = H[xi, yi]
239
+ c = c / (c.max() + 1e-9)
240
+ _wireframe_box(ax, half, n=2, seed=n * 10 + l * 3 + m)
241
+ ax.scatter(pts[:, 0], pts[:, 1], c=c, cmap="magma", s=3.2, alpha=0.75,
242
+ linewidths=0, vmin=0.0, vmax=1.0)
243
+
244
+ ax.set_xlim(-half, half)
245
+ ax.set_ylim(-half, half)
246
+ ax.set_aspect("equal")
247
+ _caption(ax, f"ORBITAL {orbital_key.upper()} PROBABILITY DENSITY VISUALIZER n={n} l={l} m={m}")
248
+
249
+ path = os.path.join(_export_dir(), out_name or f"orbital_{orbital_key}.png")
250
+ fig.savefig(path, dpi=170, facecolor=fig.get_facecolor())
251
+ plt.close(fig)
252
+ return path
253
+
254
+
255
+ # ------------------------------------------------------ 3. orbital grid --
256
+ DEFAULT_GRID = [
257
+ (2, 0, 0), (3, 0, 0),
258
+ (2, 1, 0), (3, 1, 0), (3, 1, 1),
259
+ (2, 1, 1), (3, 2, 0), (3, 2, 1), (3, 2, 2),
260
+ (4, 0, 0), (4, 1, 0), (4, 1, 1),
261
+ (4, 2, 0), (4, 2, 1), (4, 2, 2),
262
+ (4, 3, 0), (4, 3, 1), (4, 3, 2), (4, 3, 3),
263
+ ]
264
+
265
+
266
+ def render_orbital_grid(specs=None, out_name="orbital_grid.png", cols=5):
267
+ """Reference-chart-style grid of many (n,l,m) orbitals' |psi|^2,
268
+ computed as an exact deterministic density field (not sampled),
269
+ matching the classic 'Hydrogen Wave Function' probability chart."""
270
+ if not HAVE_DEPS:
271
+ return None
272
+ specs = specs or DEFAULT_GRID
273
+ rows = math.ceil(len(specs) / cols)
274
+ fig, axes = plt.subplots(rows, cols, figsize=(cols * 2.1, rows * 2.1), facecolor=BG)
275
+ axes = np.array(axes).reshape(rows, cols)
276
+
277
+ for idx, ax in enumerate(axes.flat):
278
+ _no_axes(ax)
279
+ if idx >= len(specs):
280
+ ax.axis("off")
281
+ continue
282
+ n, l, m = specs[idx]
283
+ r_max = R_MAX_FOR_N.get(n, 44) * 0.85
284
+ size = 160
285
+ xs = np.linspace(-r_max, r_max, size)
286
+ zs = np.linspace(-r_max, r_max, size)
287
+ X, Z = np.meshgrid(xs, zs)
288
+ R = np.sqrt(X ** 2 + Z ** 2) + 1e-9
289
+ TH = np.arccos(np.clip(Z / R, -1, 1))
290
+ dens = psi2(n, l, m, R, TH)
291
+ dens = dens / (dens.max() + 1e-12)
292
+ dens = dens ** 0.42 # gamma-boost so faint outer lobes stay visible
293
+ ax.imshow(dens, cmap="inferno", origin="lower",
294
+ extent=[-r_max, r_max, -r_max, r_max])
295
+ ax.set_aspect("equal")
296
+ ax.text(0.05, 0.92, f"({n},{l},{m})", transform=ax.transAxes,
297
+ color="#f2e9ff", fontsize=9, family="monospace", va="top")
298
+
299
+ fig.suptitle("Hydrogen Wave Function \u2014 probability density plots |\u03c8\u2099\u2097\u2098(r,\u03b8,\u03c6)|\u00b2",
300
+ color="#e8ebfa", fontsize=13, family="monospace", y=0.995)
301
+ fig.patch.set_facecolor(BG)
302
+ fig.subplots_adjust(wspace=0.05, hspace=0.12, top=0.94)
303
+
304
+ path = os.path.join(_export_dir("graphs"), out_name)
305
+ fig.savefig(path, dpi=160, facecolor=fig.get_facecolor())
306
+ plt.close(fig)
307
+ return path
308
+
309
+
310
+ # ---------------------------------------------- 4. chemical bonding map --
311
+ # Illustrative 2D geometries (not to scale). shells: approximate K/L
312
+ # electron-shell ring radii (a0-ish, tuned for the visual, not ab-initio).
313
+ _ELEM = {
314
+ "H": (1, "#ffffff", [(0.28, 0.10, 1.0)]),
315
+ "C": (6, "#909090", [(0.16, 0.05, 1.0), (0.55, 0.14, 0.75)]),
316
+ "N": (7, "#3050f8", [(0.15, 0.05, 1.0), (0.52, 0.13, 0.8)]),
317
+ "O": (8, "#ff0d0d", [(0.14, 0.05, 1.0), (0.50, 0.13, 0.85)]),
318
+ }
319
+
320
+ MOLECULES = {
321
+ "furan": {
322
+ "atoms": [("O", 0.00, 0.95), ("C", 0.90, 0.29), ("C", 0.56, -0.77),
323
+ ("C", -0.56, -0.77), ("C", -0.90, 0.29)],
324
+ "bonds": [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)],
325
+ },
326
+ "water": {
327
+ "atoms": [("O", 0.0, 0.35), ("H", -0.76, -0.35), ("H", 0.76, -0.35)],
328
+ "bonds": [(0, 1), (0, 2)],
329
+ },
330
+ "methane": {
331
+ "atoms": [("C", 0.0, 0.0), ("H", 0.9, 0.55), ("H", -0.9, 0.55),
332
+ ("H", 0.55, -0.95), ("H", -0.55, -0.95)],
333
+ "bonds": [(0, 1), (0, 2), (0, 3), (0, 4)],
334
+ },
335
+ "co2": {
336
+ "atoms": [("O", -1.1, 0.0), ("C", 0.0, 0.0), ("O", 1.1, 0.0)],
337
+ "bonds": [(0, 1), (1, 2)],
338
+ },
339
+ "ethanol": {
340
+ "atoms": [("C", -1.1, 0.15), ("C", 0.05, -0.45), ("O", 1.15, 0.15),
341
+ ("H", -1.9, -0.35), ("H", -1.3, 0.85), ("H", -0.9, 1.0),
342
+ ("H", 0.2, -1.1), ("H", -0.15, -1.0)],
343
+ "bonds": [(0, 1), (1, 2), (0, 3), (0, 4), (0, 5), (1, 6), (1, 7)],
344
+ },
345
+ "benzene": {
346
+ "atoms": [("C", math.cos(a), math.sin(a)) for a in
347
+ [k * math.pi / 3 for k in range(6)]],
348
+ "bonds": [(i, (i + 1) % 6) for i in range(6)],
349
+ },
350
+ }
351
+
352
+
353
+ def _elf_like_field(X, Y, mol):
354
+ field = np.zeros_like(X)
355
+ atoms = mol["atoms"]
356
+ for sym, ax_, ay_ in atoms:
357
+ _, _, shells = _ELEM.get(sym, (1, "#cccccc", [(0.2, 0.07, 1.0)]))
358
+ r = np.sqrt((X - ax_) ** 2 + (Y - ay_) ** 2)
359
+ for shell_r, width, weight in shells:
360
+ field += weight * np.exp(-((r - shell_r) / width) ** 2)
361
+ field += 0.35 * np.exp(-(r / 0.06) ** 2) # tiny core spike
362
+
363
+ for i, j in mol["bonds"]:
364
+ (sa, xa, ya), (sb, xb, yb) = atoms[i], atoms[j]
365
+ bx, by = xb - xa, yb - ya
366
+ length = math.hypot(bx, by)
367
+ ux, uy = bx / length, by / length
368
+ px, py = -uy, ux
369
+ # sample along-bond position (0..1) and perpendicular offset
370
+ along = ((X - xa) * ux + (Y - ya) * uy) / length
371
+ perp = (X - xa) * px + (Y - ya) * py
372
+ taper = np.clip(along, 0, 1)
373
+ bulge = np.sin(np.pi * np.clip(along, 0, 1)) ** 1.5
374
+ field += 0.85 * bulge * np.exp(-(perp / 0.16) ** 2) * (along > -0.05) * (along < 1.05)
375
+
376
+ return field
377
+
378
+
379
+ def render_bonding(molecule, out_name=None):
380
+ """ELF-style electron density/bonding map: concentric per-atom shell
381
+ rings + bond-region lobes, rainbow colormap on black. Stylized
382
+ illustration, not an ab-initio ELF calculation (see module docstring)."""
383
+ if not HAVE_DEPS:
384
+ return None
385
+ key = molecule.strip().lower()
386
+ if key not in MOLECULES:
387
+ return None
388
+ mol = MOLECULES[key]
389
+ xs = [a[1] for a in mol["atoms"]]
390
+ ys = [a[2] for a in mol["atoms"]]
391
+ pad = 0.9
392
+ xmin, xmax = min(xs) - pad, max(xs) + pad
393
+ ymin, ymax = min(ys) - pad, max(ys) + pad
394
+ size = 420
395
+ X, Y = np.meshgrid(np.linspace(xmin, xmax, size), np.linspace(ymin, ymax, size))
396
+ field = _elf_like_field(X, Y, mol)
397
+ field = field / (field.max() + 1e-12)
398
+ field = gaussian_filter(field, sigma=1.0)
399
+ field = np.clip(field, 0, 1)
400
+
401
+ fig, ax = plt.subplots(figsize=(6.6, 6.6), facecolor=BG)
402
+ _no_axes(ax)
403
+ im = ax.imshow(field, cmap="turbo", origin="lower",
404
+ extent=[xmin, xmax, ymin, ymax], vmin=0, vmax=1)
405
+ for sym, ax_, ay_ in mol["atoms"]:
406
+ ax.text(ax_, ay_, sym, color="#ffffff", fontsize=10, family="monospace",
407
+ ha="center", va="center", weight="bold",
408
+ bbox=dict(boxstyle="circle,pad=0.15", fc="#000000", ec="none", alpha=0.35))
409
+ ax.set_aspect("equal")
410
+ ax.set_xlim(xmin, xmax)
411
+ ax.set_ylim(ymin, ymax)
412
+
413
+ cbar = fig.colorbar(im, ax=ax, fraction=0.045, pad=0.02)
414
+ cbar.set_label("ELF (stylized)", color="#c0caf5", family="monospace", fontsize=9)
415
+ cbar.ax.yaxis.set_tick_params(color="#c0caf5")
416
+ plt.setp(cbar.ax.get_yticklabels(), color="#c0caf5", family="monospace", fontsize=8)
417
+
418
+ _caption(ax, f"{molecule.upper()} \u2014 ELECTRON LOCALIZATION (STYLIZED) \u00b7 BONDING DENSITY MAP")
419
+
420
+ path = os.path.join(_export_dir("graphs"), out_name or f"bonding_{key}.png")
421
+ fig.savefig(path, dpi=170, facecolor=fig.get_facecolor())
422
+ plt.close(fig)
423
+ return path
424
+
425
+
426
+ # --------------------------------------------------- 5. Bloch sphere --
427
+ def bloch_state_probs(theta, phi):
428
+ """Exact single-qubit measurement probabilities for
429
+ |psi> = cos(theta/2)|0> + e^{i phi} sin(theta/2)|1>, theta/phi in
430
+ RADIANS. Returns a dict of (basis -> (p_plus, p_minus)) for the
431
+ Z, X, and Y bases — real quantum mechanics, not an approximation:
432
+ Z: {|0>,|1>}, X: {|+>,|->} = (|0>+/-|1>)/sqrt(2),
433
+ Y: {|+i>,|-i>} = (|0>+/-i|1>)/sqrt(2).
434
+ """
435
+ c, s = math.cos(theta / 2.0), math.sin(theta / 2.0)
436
+ alpha = complex(c, 0.0)
437
+ beta = complex(s * math.cos(phi), s * math.sin(phi)) # s * e^{i phi}
438
+
439
+ p0 = abs(alpha) ** 2
440
+ p1 = abs(beta) ** 2
441
+
442
+ # X basis: |+> = (|0>+|1>)/sqrt2, |-> = (|0>-|1>)/sqrt2
443
+ amp_plus = (alpha + beta) / math.sqrt(2)
444
+ amp_minus = (alpha - beta) / math.sqrt(2)
445
+ px_plus, px_minus = abs(amp_plus) ** 2, abs(amp_minus) ** 2
446
+
447
+ # Y basis: |+i> = (|0>+i|1>)/sqrt2, |-i> = (|0>-i|1>)/sqrt2.
448
+ # Projection amplitude is <+i|psi> = (<0| - i<1|)/sqrt2 . psi -- the
449
+ # bra conjugates the i in the ket to -i. (Caught by testing: with the
450
+ # sign the other way around, theta=90,phi=90 -- which IS |+i> by the
451
+ # definition above -- came out reporting 100% probability of being
452
+ # |-i> instead of |+i>. This is the corrected, verified version.)
453
+ amp_pi = (alpha - 1j * beta) / math.sqrt(2)
454
+ amp_mi = (alpha + 1j * beta) / math.sqrt(2)
455
+ py_plus, py_minus = abs(amp_pi) ** 2, abs(amp_mi) ** 2
456
+
457
+ return {
458
+ "Z": (p0, p1),
459
+ "X": (px_plus, px_minus),
460
+ "Y": (py_plus, py_minus),
461
+ }
462
+
463
+
464
+ def bloch_vector(theta, phi):
465
+ """The real-space Bloch-sphere point (x,y,z) for the state at
466
+ (theta, phi) — the standard identity that a qubit's density matrix
467
+ rho = (I + x*sigma_x + y*sigma_y + z*sigma_z)/2 has
468
+ (x,y,z) = (sin(theta)cos(phi), sin(theta)sin(phi), cos(theta))."""
469
+ return (math.sin(theta) * math.cos(phi),
470
+ math.sin(theta) * math.sin(phi),
471
+ math.cos(theta))
472
+
473
+
474
+ def render_bloch_sphere(theta_deg=90.0, phi_deg=0.0, out_name=None):
475
+ """Bloch sphere for |psi> = cos(theta/2)|0> + e^{i phi}sin(theta/2)|1>,
476
+ theta_deg/phi_deg in DEGREES (matching how people naturally specify
477
+ them, e.g. '/bloch 90 0' for the |+> state). Draws the sphere, the
478
+ state vector, and the exact Z/X/Y measurement-probability bars —
479
+ same information content as a standard qubit-state visualizer, all
480
+ computed from bloch_state_probs()/bloch_vector() above, not faked."""
481
+ if not HAVE_DEPS:
482
+ return None
483
+ theta = math.radians(theta_deg % 360.0)
484
+ phi = math.radians(phi_deg % 360.0)
485
+ x, y, z = bloch_vector(theta, phi)
486
+ probs = bloch_state_probs(theta, phi)
487
+
488
+ fig = plt.figure(figsize=(10.5, 5.2), facecolor=BG)
489
+ ax3d = fig.add_subplot(1, 2, 1, projection="3d")
490
+ ax3d.set_facecolor(BG)
491
+ fig.patch.set_facecolor(BG)
492
+
493
+ # wireframe sphere
494
+ u = np.linspace(0, 2 * math.pi, 40)
495
+ v = np.linspace(0, math.pi, 24)
496
+ sx = np.outer(np.cos(u), np.sin(v))
497
+ sy = np.outer(np.sin(u), np.sin(v))
498
+ sz = np.outer(np.ones_like(u), np.cos(v))
499
+ ax3d.plot_wireframe(sx, sy, sz, color="#3b4261", linewidth=0.4, alpha=0.6)
500
+
501
+ # equator + prime meridian for orientation
502
+ t = np.linspace(0, 2 * math.pi, 200)
503
+ ax3d.plot(np.cos(t), np.sin(t), 0, color="#565f89", lw=0.8)
504
+ ax3d.plot(np.cos(t), np.zeros_like(t), np.sin(t), color="#414868", lw=0.6)
505
+
506
+ # axis lines + basis-state labels
507
+ axis_len = 1.35
508
+ for (dx, dy, dz), label in [
509
+ ((0, 0, 1), "|0\u27e9"), ((0, 0, -1), "|1\u27e9"),
510
+ ((1, 0, 0), "|+\u27e9"), ((-1, 0, 0), "|\u2212\u27e9"),
511
+ ((0, 1, 0), "|+i\u27e9"), ((0, -1, 0), "|\u2212i\u27e9"),
512
+ ]:
513
+ ax3d.plot([0, dx * axis_len], [0, dy * axis_len], [0, dz * axis_len],
514
+ color="#565f89", lw=0.7)
515
+ ax3d.text(dx * axis_len * 1.15, dy * axis_len * 1.15, dz * axis_len * 1.15,
516
+ label, color="#c0caf5", fontsize=9, family="monospace", ha="center")
517
+
518
+ # the actual state vector
519
+ ax3d.plot([0, x], [0, y], [0, z], color="#ff9e64", lw=2.6)
520
+ ax3d.scatter([x], [y], [z], color="#ff9e64", s=60, depthshade=False)
521
+
522
+ ax3d.set_xlim(-1.2, 1.2); ax3d.set_ylim(-1.2, 1.2); ax3d.set_zlim(-1.2, 1.2)
523
+ ax3d.set_box_aspect((1, 1, 1))
524
+ ax3d.set_axis_off()
525
+ ax3d.set_title(f"|\u03c8\u27e9 = cos(\u03b8/2)|0\u27e9 + e^{{i\u03c6}}sin(\u03b8/2)|1\u27e9"
526
+ f" \u03b8={theta_deg:.0f}\u00b0 \u03c6={phi_deg:.0f}\u00b0",
527
+ color="#c0caf5", fontsize=9.5, family="monospace", pad=0)
528
+
529
+ # measurement-probability bars, exact values from bloch_state_probs
530
+ ax2d = fig.add_subplot(1, 2, 2)
531
+ _no_axes(ax2d)
532
+ bases = ["Z", "X", "Y"]
533
+ plus_labels = {"Z": "|0\u27e9", "X": "|+\u27e9", "Y": "|+i\u27e9"}
534
+ minus_labels = {"Z": "|1\u27e9", "X": "|\u2212\u27e9", "Y": "|\u2212i\u27e9"}
535
+ bar_colors = {"Z": "#7dcfff", "X": "#ff7f9a", "Y": "#9ece6a"}
536
+ xpos = np.arange(len(bases)) * 2.2
537
+ for i, b in enumerate(bases):
538
+ pplus, pminus = probs[b]
539
+ ax2d.bar(xpos[i] - 0.35, pplus, width=0.6, color=bar_colors[b], alpha=0.95)
540
+ ax2d.bar(xpos[i] + 0.35, pminus, width=0.6, color=bar_colors[b], alpha=0.45)
541
+ ax2d.text(xpos[i] - 0.35, pplus + 0.03, f"{pplus*100:.1f}%", ha="center",
542
+ color="#c0caf5", fontsize=8, family="monospace")
543
+ ax2d.text(xpos[i] + 0.35, pminus + 0.03, f"{pminus*100:.1f}%", ha="center",
544
+ color="#c0caf5", fontsize=8, family="monospace")
545
+ ax2d.text(xpos[i] - 0.35, -0.09, plus_labels[b], ha="center",
546
+ color="#c0caf5", fontsize=8, family="monospace")
547
+ ax2d.text(xpos[i] + 0.35, -0.09, minus_labels[b], ha="center",
548
+ color="#c0caf5", fontsize=8, family="monospace")
549
+ ax2d.text(xpos[i], 1.12, f"{b}-basis", ha="center",
550
+ color="#e8ebfa", fontsize=9.5, family="monospace", weight="bold")
551
+ ax2d.set_ylim(-0.18, 1.25)
552
+ ax2d.set_xlim(-1.2, xpos[-1] + 1.2)
553
+ _caption(ax2d, "EXACT MEASUREMENT PROBABILITIES (Born rule, |amplitude|\u00b2)")
554
+
555
+ path = os.path.join(_export_dir("simulations"), out_name or "bloch_sphere.png")
556
+ fig.savefig(path, dpi=170, facecolor=fig.get_facecolor())
557
+ plt.close(fig)
558
+ return path
@@ -0,0 +1,126 @@
1
+ """
2
+ CCT Security Scanner — real static analysis (Python `ast` module, not
3
+ regex guessing) over code before it's run from /codepad, flagging
4
+ patterns that can escape the sandbox, touch the filesystem/network, or
5
+ are otherwise risky to execute automatically. This does not "fix" code
6
+ or pretend to catch everything static analysis fundamentally can't
7
+ catch (e.g. cleverly obfuscated payloads) — it's a real first pass, the
8
+ same category of check `bandit` does, sized for this app.
9
+ """
10
+
11
+ import ast
12
+
13
+ HIGH = "high"
14
+ MEDIUM = "medium"
15
+ LOW = "low"
16
+
17
+ _DANGEROUS_CALLS = {
18
+ "eval": HIGH, "exec": HIGH, "compile": MEDIUM,
19
+ "__import__": MEDIUM,
20
+ }
21
+ _DANGEROUS_MODULES = {
22
+ "os": MEDIUM, "subprocess": HIGH, "shutil": MEDIUM,
23
+ "socket": HIGH, "ctypes": HIGH, "multiprocessing": MEDIUM,
24
+ "pickle": HIGH, "marshal": HIGH, "importlib": MEDIUM,
25
+ "sys": LOW,
26
+ }
27
+ _DANGEROUS_ATTRS = {
28
+ "system": HIGH, "popen": HIGH, "spawn": HIGH, "remove": MEDIUM,
29
+ "rmtree": HIGH, "unlink": MEDIUM, "chmod": MEDIUM, "kill": HIGH,
30
+ "fork": HIGH, "loads": MEDIUM, # pickle.loads / marshal.loads
31
+ }
32
+
33
+
34
+ class Finding:
35
+ __slots__ = ("severity", "line", "message")
36
+
37
+ def __init__(self, severity, line, message):
38
+ self.severity = severity
39
+ self.line = line
40
+ self.message = message
41
+
42
+ def __repr__(self):
43
+ return f"[{self.severity.upper()}] line {self.line}: {self.message}"
44
+
45
+
46
+ def scan_code(code):
47
+ """Return a list of Finding objects. Never raises on syntactically
48
+ broken code — a SyntaxError just becomes a single LOW finding, since
49
+ code that doesn't parse can't be analysed further (and will fail to
50
+ run anyway, which is not a security issue by itself)."""
51
+ findings = []
52
+ try:
53
+ tree = ast.parse(code)
54
+ except SyntaxError as e:
55
+ return [Finding(LOW, getattr(e, "lineno", 0) or 0, f"Syntax error: {e.msg}")]
56
+
57
+ imported_names = {} # local name -> real module
58
+
59
+ for node in ast.walk(tree):
60
+ if isinstance(node, ast.Import):
61
+ for alias in node.names:
62
+ mod = alias.name.split(".")[0]
63
+ imported_names[alias.asname or alias.name] = mod
64
+ if mod in _DANGEROUS_MODULES:
65
+ findings.append(Finding(
66
+ _DANGEROUS_MODULES[mod], node.lineno,
67
+ f"imports '{mod}' — can affect the host system if misused"))
68
+ elif isinstance(node, ast.ImportFrom):
69
+ mod = (node.module or "").split(".")[0]
70
+ for alias in node.names:
71
+ imported_names[alias.asname or alias.name] = mod
72
+ if mod in _DANGEROUS_MODULES:
73
+ findings.append(Finding(
74
+ _DANGEROUS_MODULES[mod], node.lineno,
75
+ f"imports from '{mod}' — can affect the host system if misused"))
76
+ elif isinstance(node, ast.Call):
77
+ fn = node.func
78
+ if isinstance(fn, ast.Name) and fn.id in _DANGEROUS_CALLS:
79
+ findings.append(Finding(
80
+ _DANGEROUS_CALLS[fn.id], node.lineno,
81
+ f"calls {fn.id}(...) — dynamic code execution"))
82
+ elif isinstance(fn, ast.Attribute) and fn.attr in _DANGEROUS_ATTRS:
83
+ base = fn.value.id if isinstance(fn.value, ast.Name) else "?"
84
+ findings.append(Finding(
85
+ _DANGEROUS_ATTRS[fn.attr], node.lineno,
86
+ f"calls {base}.{fn.attr}(...) — potentially dangerous operation"))
87
+ elif isinstance(node, ast.Attribute) and node.attr in ("__globals__", "__subclasses__", "__bases__"):
88
+ findings.append(Finding(
89
+ MEDIUM, node.lineno,
90
+ f"accesses {node.attr} — a classic sandbox-escape technique"))
91
+
92
+ # open(..., mode with 'w'/'a') — filesystem writes
93
+ for node in ast.walk(tree):
94
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "open":
95
+ mode = None
96
+ if len(node.args) >= 2 and isinstance(node.args[1], ast.Constant):
97
+ mode = node.args[1].value
98
+ for kw in node.keywords:
99
+ if kw.arg == "mode" and isinstance(kw.value, ast.Constant):
100
+ mode = kw.value.value
101
+ if mode and any(c in str(mode) for c in "wax+"):
102
+ findings.append(Finding(
103
+ MEDIUM, node.lineno, f"opens a file for writing (mode='{mode}')"))
104
+
105
+ return findings
106
+
107
+
108
+ def risk_level(findings):
109
+ if any(f.severity == HIGH for f in findings):
110
+ return HIGH
111
+ if any(f.severity == MEDIUM for f in findings):
112
+ return MEDIUM
113
+ if findings:
114
+ return LOW
115
+ return "none"
116
+
117
+
118
+ def summarize(findings):
119
+ """Short human-readable summary line for a permission card."""
120
+ if not findings:
121
+ return "No risky patterns detected."
122
+ counts = {}
123
+ for f in findings:
124
+ counts[f.severity] = counts.get(f.severity, 0) + 1
125
+ parts = [f"{counts[s]} {s}" for s in (HIGH, MEDIUM, LOW) if s in counts]
126
+ return ", ".join(parts) + " finding(s)"