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,358 @@
1
+ """
2
+ Graphing for Chemistry Calc Terminal.
3
+
4
+ A real terminal can't natively draw a rotatable 3D plot, so this module
5
+ takes the honest, practical two-track approach:
6
+
7
+ * ascii_plot() -- live, animated (pen draws left-to-right) 2D
8
+ character-grid plot rendered directly in the
9
+ terminal. No dependencies.
10
+
11
+ * export_2d/3d() -- high quality PNG rendered with matplotlib
12
+ (Agg backend, no display needed) for anyone
13
+ who wants a crisp 2D curve or a real rotatable-
14
+ looking 3D surface, saved to disk and opened
15
+ with the OS default viewer.
16
+
17
+ Preset curves cover the numericals already in the notebook engine so a
18
+ graph is one keystroke away from any solved topic.
19
+ """
20
+
21
+ import math
22
+ import os
23
+ import re
24
+ import sys
25
+ import time
26
+
27
+ from . import theme
28
+
29
+ EXPORT_DIR = os.path.join(os.path.expanduser("~"), "cct_exports")
30
+
31
+ # ---------------------------------------------------------- custom / AI --
32
+ # The AI agent (and anyone typing a formula) can plot or export ANY named
33
+ # 2D function or 3D surface, not just the 10 built-in presets. This uses
34
+ # the same restricted-character + restricted-namespace `eval` approach as
35
+ # the quick calculator and the symbolic solver's constant table, so no
36
+ # arbitrary code execution is possible.
37
+ _MATH_NS = {
38
+ "sin": math.sin, "cos": math.cos, "tan": math.tan, "asin": math.asin,
39
+ "acos": math.acos, "atan": math.atan, "exp": math.exp, "log": math.log,
40
+ "log10": math.log10, "sqrt": math.sqrt, "pi": math.pi, "e": math.e,
41
+ "abs": abs, "pow": pow,
42
+ }
43
+ _EXPR_RE = re.compile(r"^[0-9A-Za-z_+\-*/().,\s]*$")
44
+
45
+
46
+ class ExpressionError(Exception):
47
+ pass
48
+
49
+
50
+ def _compile_expr(expr_str, varnames):
51
+ """Compile a user/AI-supplied math expression into a callable f(*vars).
52
+ Only arithmetic, the functions in _MATH_NS, and the given variable
53
+ names are reachable — no builtins, no attribute access."""
54
+ if not expr_str or not _EXPR_RE.match(expr_str.replace("^", "")):
55
+ raise ExpressionError(f"Invalid or unsafe expression: {expr_str!r}")
56
+ code = compile(expr_str.replace("^", "**"), "<expr>", "eval")
57
+
58
+ def f(*args):
59
+ ns = dict(_MATH_NS)
60
+ ns.update(dict(zip(varnames, args)))
61
+ return eval(code, {"__builtins__": {}}, ns)
62
+
63
+ # fail fast on a syntactically-valid-looking but broken expression
64
+ try:
65
+ f(*([1.0] * len(varnames)))
66
+ except ExpressionError:
67
+ raise
68
+ except Exception as exc:
69
+ raise ExpressionError(f"Could not evaluate expression: {exc}")
70
+ return f
71
+
72
+
73
+ def custom_curve(expr, xmin=0.0, xmax=10.0, points=60, title=None,
74
+ xlabel="x", ylabel="y", var="x"):
75
+ """Build (xs, ys, title, xlabel, ylabel) for ANY named function of one
76
+ variable, e.g. custom_curve('sin(x)*exp(-x/5)', 0, 20, title='Damped wave').
77
+ This is what lets the AI agent — or a person — graph something that
78
+ isn't one of the 10 built-in chemistry presets."""
79
+ xmin, xmax = float(xmin), float(xmax)
80
+ if xmax <= xmin:
81
+ xmax = xmin + 1.0
82
+ f = _compile_expr(expr, [var])
83
+ xs = [xmin + i * (xmax - xmin) / points for i in range(points + 1)]
84
+ ys = []
85
+ for x in xs:
86
+ try:
87
+ val = float(f(x))
88
+ except Exception:
89
+ val = 0.0
90
+ ys.append(val if math.isfinite(val) else 0.0)
91
+ return xs, ys, title or f"{ylabel} = {expr}", xlabel, ylabel
92
+
93
+
94
+ def export_custom_2d(expr, xmin=0.0, xmax=10.0, points=300, title=None,
95
+ xlabel="x", ylabel="y", filename=None):
96
+ xs, ys, t, xl, yl = custom_curve(expr, xmin, xmax, points, title, xlabel, ylabel)
97
+ return export_2d(xs, ys, t, xl, yl, filename)
98
+
99
+
100
+ def export_custom_3d(expr, xmin=-5.0, xmax=5.0, ymin=-5.0, ymax=5.0,
101
+ points=45, title=None, filename=None):
102
+ """Export ANY named 3D surface z = f(x, y) as a high-quality PNG —
103
+ the free-form counterpart to export_3d_surface()'s two fixed presets."""
104
+ import numpy as np
105
+ import matplotlib
106
+ matplotlib.use("Agg")
107
+ import matplotlib.pyplot as plt
108
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401
109
+ _ensure_dir()
110
+
111
+ f = _compile_expr(expr, ["x", "y"])
112
+ xs = np.linspace(float(xmin), float(xmax), points)
113
+ ys = np.linspace(float(ymin), float(ymax), points)
114
+ X, Y = np.meshgrid(xs, ys)
115
+ Z = np.zeros_like(X)
116
+ for i in range(X.shape[0]):
117
+ for j in range(X.shape[1]):
118
+ try:
119
+ v = float(f(X[i, j], Y[i, j]))
120
+ Z[i, j] = v if math.isfinite(v) else 0.0
121
+ except Exception:
122
+ Z[i, j] = 0.0
123
+
124
+ fig = plt.figure(figsize=(7, 6), facecolor="#1a1b26")
125
+ ax = fig.add_subplot(111, projection="3d")
126
+ ax.set_facecolor("#1a1b26")
127
+ ax.plot_surface(X, Y, Z, cmap="viridis", edgecolor="none")
128
+ title = title or f"z = {expr}"
129
+ ax.set_title(title, color="#c0caf5")
130
+ ax.set_xlabel("x", color="#a9b1d6")
131
+ ax.set_ylabel("y", color="#a9b1d6")
132
+ ax.set_zlabel("z", color="#a9b1d6")
133
+ ax.tick_params(colors="#a9b1d6")
134
+
135
+ safe_name = re.sub(r"[^a-zA-Z0-9]+", "_", title.lower()).strip("_")[:40]
136
+ filename = filename or f"{safe_name or 'custom_surface'}.png"
137
+ path = os.path.join(EXPORT_DIR, filename)
138
+ fig.tight_layout()
139
+ fig.savefig(path, dpi=150)
140
+ plt.close(fig)
141
+ return path
142
+
143
+
144
+ # --------------------------------------------------------------- presets --
145
+ def preset_curve(name, **params):
146
+ """Returns (x_list, y_list, title, xlabel, ylabel) for a named
147
+ chemistry curve, computed from the real governing formula."""
148
+ n = 60
149
+ if name == "zero_order":
150
+ R0 = params.get("R0", 1.0); k = params.get("k", 0.02); tmax = params.get("tmax", 40)
151
+ xs = [i * tmax / n for i in range(n + 1)]
152
+ ys = [max(0.0, R0 - k * t) for t in xs]
153
+ return xs, ys, "Zero Order: [R] vs t", "time", "[R]"
154
+ if name == "first_order":
155
+ R0 = params.get("R0", 1.0); k = params.get("k", 0.05); tmax = params.get("tmax", 60)
156
+ xs = [i * tmax / n for i in range(n + 1)]
157
+ ys = [R0 * math.exp(-k * t) for t in xs]
158
+ return xs, ys, "First Order: [R] vs t", "time", "[R]"
159
+ if name == "first_order_log":
160
+ R0 = params.get("R0", 1.0); k = params.get("k", 0.05); tmax = params.get("tmax", 60)
161
+ xs = [i * tmax / n for i in range(n + 1)]
162
+ ys = [math.log10(max(R0 * math.exp(-k * t), 1e-12)) for t in xs]
163
+ return xs, ys, "First Order: log[R] vs t (straight line)", "time", "log[R]"
164
+ if name == "second_order":
165
+ R0 = params.get("R0", 1.0); k = params.get("k", 0.05); tmax = params.get("tmax", 60)
166
+ xs = [i * tmax / n for i in range(n + 1)]
167
+ ys = [1.0 / (1.0 / R0 + k * t) for t in xs]
168
+ return xs, ys, "Second Order: [R] vs t", "time", "[R]"
169
+ if name == "arrhenius":
170
+ A = params.get("A", 1e13); Ea = params.get("Ea", 75000)
171
+ Ts = [i for i in range(250, 251 + n * 4, 4)]
172
+ xs = [1.0 / T for T in Ts]
173
+ ys = [math.log(A * math.exp(-Ea / (8.314 * T))) for T in Ts]
174
+ return xs, ys, "Arrhenius: ln k vs 1/T", "1/T", "ln k"
175
+ if name == "boyles":
176
+ P1 = params.get("P1", 1.0); V1 = params.get("V1", 22.4)
177
+ xs = [0.5 + i * (10 - 0.5) / n for i in range(n + 1)]
178
+ ys = [(P1 * V1) / P for P in xs]
179
+ return xs, ys, "Boyle's Law: V vs P (T const.)", "P", "V"
180
+ if name == "maxwell_boltzmann":
181
+ T = params.get("T", 300); M = params.get("M", 0.028)
182
+ R = 8.314
183
+ xs = [i * 2500 / n for i in range(1, n + 1)]
184
+ ys = [4 * math.pi * (M / (2 * math.pi * R * T)) ** 1.5 * v * v *
185
+ math.exp(-M * v * v / (2 * R * T)) for v in xs]
186
+ return xs, ys, f"Maxwell-Boltzmann speed distribution (T={T}K)", "speed (m/s)", "f(v)"
187
+ if name == "radial_prob_1s":
188
+ xs = [i * 6 / n for i in range(n + 1)]
189
+ ys = [(r * r) * math.exp(-2 * r) for r in xs]
190
+ return xs, ys, "1s Radial Probability Density", "r (a0)", "r\u00b2R\u00b2"
191
+ if name == "titration":
192
+ Ca = params.get("Ca", 0.1); Va = params.get("Va", 25.0); Cb = params.get("Cb", 0.1)
193
+ xs = [i * 60 / n for i in range(1, n)]
194
+ ys = []
195
+ for Vb in xs:
196
+ moles_a = Ca * Va; moles_b = Cb * Vb
197
+ excess = moles_b - moles_a
198
+ total_v = Va + Vb
199
+ if excess > 0.0001:
200
+ pOH = -math.log10(excess / total_v)
201
+ ys.append(14 - pOH)
202
+ elif excess < -0.0001:
203
+ H = -excess / total_v
204
+ ys.append(-math.log10(H))
205
+ else:
206
+ ys.append(7.0)
207
+ return xs, ys, "Titration Curve: pH vs Volume of Base", "V base (mL)", "pH"
208
+ if name == "energy_levels":
209
+ xs = list(range(1, 6))
210
+ ys = [-2.18e-18 / (n_ * n_) for n_ in xs]
211
+ return xs, ys, "Bohr Energy Levels (Hydrogen)", "n", "E (J)"
212
+ raise ValueError(f"Unknown preset: {name}")
213
+
214
+
215
+ PRESETS = [
216
+ ("zero_order", "Zero Order kinetics: [R] vs t"),
217
+ ("first_order", "First Order kinetics: [R] vs t (exponential decay)"),
218
+ ("first_order_log", "First Order: log[R] vs t (linear form)"),
219
+ ("second_order", "Second Order kinetics: [R] vs t"),
220
+ ("arrhenius", "Arrhenius plot: ln k vs 1/T"),
221
+ ("boyles", "Boyle's Law: V vs P"),
222
+ ("maxwell_boltzmann", "Maxwell-Boltzmann speed distribution"),
223
+ ("radial_prob_1s", "1s orbital radial probability density"),
224
+ ("titration", "Strong acid / strong base titration curve"),
225
+ ("energy_levels", "Bohr hydrogen energy levels"),
226
+ ]
227
+
228
+
229
+ # ------------------------------------------------------------ ascii plot --
230
+ def ascii_plot(xs, ys, title="", xlabel="x", ylabel="y", width=64, height=18,
231
+ animate=True, out=sys.stdout, color=None):
232
+ color = color or theme.CYAN
233
+ ymin, ymax = min(ys), max(ys)
234
+ xmin, xmax = min(xs), max(xs)
235
+ if ymax == ymin:
236
+ ymax += 1
237
+ if xmax == xmin:
238
+ xmax += 1
239
+
240
+ grid = [[" "] * width for _ in range(height)]
241
+
242
+ def gx(x): return int((x - xmin) / (xmax - xmin) * (width - 1))
243
+ def gy(y): return int(height - 1 - (y - ymin) / (ymax - ymin) * (height - 1))
244
+
245
+ points = [(gx(x), gy(y)) for x, y in zip(xs, ys)]
246
+
247
+ def draw_upto(idx):
248
+ for i in range(1, idx + 1):
249
+ x0, y0 = points[i - 1]
250
+ x1, y1 = points[i]
251
+ steps = max(abs(x1 - x0), abs(y1 - y0), 1)
252
+ for s in range(steps + 1):
253
+ xx = round(x0 + (x1 - x0) * s / steps)
254
+ yy = round(y0 + (y1 - y0) * s / steps)
255
+ if 0 <= yy < height and 0 <= xx < width:
256
+ grid[yy][xx] = "\u25cf"
257
+
258
+ def render():
259
+ lines = []
260
+ y_at = lambda row: ymax - row * (ymax - ymin) / (height - 1)
261
+ for r, row in enumerate(grid):
262
+ label = f"{y_at(r):>9.3g} |" if r % 3 == 0 else " " * 9 + " |"
263
+ lines.append(theme.faint(label) + theme.fg("".join(row), color))
264
+ axis = " " * 10 + "-" * width
265
+ lines.append(theme.faint(axis))
266
+ lines.append(" " * 10 + theme.dim(f"{xmin:.3g}") +
267
+ " " * (width - len(f"{xmin:.3g}") - len(f"{xmax:.3g}")) +
268
+ theme.dim(f"{xmax:.3g}"))
269
+ return lines
270
+
271
+ if title:
272
+ print(theme.purple(title, bold=True))
273
+ if animate:
274
+ for i in range(1, len(points), max(1, len(points) // 40)):
275
+ draw_upto(i)
276
+ out.write("\033[H" if i > 1 else "")
277
+ for l in render():
278
+ out.write(l + "\n")
279
+ out.flush()
280
+ time.sleep(0.02)
281
+ draw_upto(len(points) - 1)
282
+ for l in render():
283
+ print(l)
284
+ print(theme.dim(f" x: {xlabel} y: {ylabel}"))
285
+
286
+
287
+ # --------------------------------------------------------- image export --
288
+ def _ensure_dir():
289
+ os.makedirs(EXPORT_DIR, exist_ok=True)
290
+
291
+
292
+ def export_2d(xs, ys, title, xlabel, ylabel, filename=None):
293
+ import matplotlib
294
+ matplotlib.use("Agg")
295
+ import matplotlib.pyplot as plt
296
+ _ensure_dir()
297
+ filename = filename or (title.lower().replace(" ", "_").replace(":", "") + ".png")
298
+ path = os.path.join(EXPORT_DIR, filename)
299
+ fig, ax = plt.subplots(figsize=(7, 4.5), facecolor="#1a1b26")
300
+ ax.set_facecolor("#1a1b26")
301
+ ax.plot(xs, ys, color="#7dcfff", linewidth=2)
302
+ ax.set_title(title, color="#c0caf5")
303
+ ax.set_xlabel(xlabel, color="#a9b1d6")
304
+ ax.set_ylabel(ylabel, color="#a9b1d6")
305
+ ax.tick_params(colors="#a9b1d6")
306
+ for spine in ax.spines.values():
307
+ spine.set_color("#414868")
308
+ ax.grid(True, color="#292e42", linewidth=0.5)
309
+ fig.tight_layout()
310
+ fig.savefig(path, dpi=150)
311
+ plt.close(fig)
312
+ return path
313
+
314
+
315
+ def export_3d_surface(kind="orbital_3d", filename=None):
316
+ """Renders a real 3D surface (matplotlib) for a chemistry concept:
317
+ 'orbital_3d' -> a 2p-orbital-shaped probability surface,
318
+ 'pvt' -> the ideal-gas P-V-T surface."""
319
+ import numpy as np
320
+ import matplotlib
321
+ matplotlib.use("Agg")
322
+ import matplotlib.pyplot as plt
323
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401
324
+ _ensure_dir()
325
+
326
+ fig = plt.figure(figsize=(7, 6), facecolor="#1a1b26")
327
+ ax = fig.add_subplot(111, projection="3d")
328
+ ax.set_facecolor("#1a1b26")
329
+
330
+ if kind == "pvt":
331
+ n_ = np.linspace(0.5, 3, 40)
332
+ T = np.linspace(200, 500, 40)
333
+ N, T2 = np.meshgrid(n_, T)
334
+ P = N * 0.0821 * T2 / 10.0 # V fixed at 10 L
335
+ surf = ax.plot_surface(N, T2, P, cmap="plasma", edgecolor="none")
336
+ ax.set_xlabel("n (mol)", color="#a9b1d6")
337
+ ax.set_ylabel("T (K)", color="#a9b1d6")
338
+ ax.set_zlabel("P (atm)", color="#a9b1d6")
339
+ ax.set_title("Ideal Gas Law surface: P(n, T) at V=10L", color="#c0caf5")
340
+ filename = filename or "pvt_surface.png"
341
+ else:
342
+ theta = np.linspace(0, np.pi, 60)
343
+ phi = np.linspace(0, 2 * np.pi, 60)
344
+ TH, PH = np.meshgrid(theta, phi)
345
+ r = np.abs(np.cos(TH)) # 2pz angular shape
346
+ X = r * np.sin(TH) * np.cos(PH)
347
+ Y = r * np.sin(TH) * np.sin(PH)
348
+ Z = r * np.cos(TH)
349
+ surf = ax.plot_surface(X, Y, Z, cmap="cool", edgecolor="none")
350
+ ax.set_title("2p Orbital — angular probability surface", color="#c0caf5")
351
+ filename = filename or "orbital_2p_3d.png"
352
+
353
+ ax.tick_params(colors="#a9b1d6")
354
+ path = os.path.join(EXPORT_DIR, filename)
355
+ fig.tight_layout()
356
+ fig.savefig(path, dpi=150)
357
+ plt.close(fig)
358
+ return path