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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
calc_terminal/scires.py
ADDED
|
@@ -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)"
|