dlspit 2.2.0__tar.gz → 2.3.0__tar.gz

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 (71) hide show
  1. {dlspit-2.2.0 → dlspit-2.3.0}/PKG-INFO +48 -2
  2. {dlspit-2.2.0 → dlspit-2.3.0}/README.md +47 -1
  3. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/__init__.py +13 -2
  4. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/__main__.py +1 -1
  5. dlspit-2.3.0/dl_experiments/display.py +229 -0
  6. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/registry.py +5 -0
  7. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/registry.py +5 -0
  8. {dlspit-2.2.0 → dlspit-2.3.0}/pyproject.toml +1 -1
  9. dlspit-2.3.0/tests/test_display.py +245 -0
  10. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/__init__.py +0 -0
  11. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/__init__.py +0 -0
  12. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp10_transfer.py +0 -0
  13. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp1a_mcculloch_pitts.py +0 -0
  14. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp1b_mlp_binary.py +0 -0
  15. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp2a_activations.py +0 -0
  16. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp2b_backprop_mnist.py +0 -0
  17. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp3_regularisation.py +0 -0
  18. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp4_cnn.py +0 -0
  19. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp5_resnet.py +0 -0
  20. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp6_rnn.py +0 -0
  21. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp7_lstm.py +0 -0
  22. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp8_transformer.py +0 -0
  23. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp9_gan.py +0 -0
  24. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/experiments/exp9b_vae.py +0 -0
  25. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/dl/registry.py +0 -0
  26. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/__init__.py +0 -0
  27. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/_nn.py +0 -0
  28. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/_viz.py +0 -0
  29. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/chatbot.py +0 -0
  30. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/cyclegan.py +0 -0
  31. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/diffusion.py +0 -0
  32. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/face_swap.py +0 -0
  33. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/gan.py +0 -0
  34. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/mini_gpt.py +0 -0
  35. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/music_generation.py +0 -0
  36. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/text_to_image.py +0 -0
  37. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/genai/vae.py +0 -0
  38. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/__init__.py +0 -0
  39. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/__init__.py +0 -0
  40. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/decision_tree.py +0 -0
  41. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/eda_titanic.py +0 -0
  42. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/kmeans_clustering.py +0 -0
  43. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/linear_regression.py +0 -0
  44. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/random_forest.py +0 -0
  45. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/svm_classifier.py +0 -0
  46. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/svm_rbf_kernel.py +0 -0
  47. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/experiments/xgboost_classifier.py +0 -0
  48. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/ml_ese/registry.py +0 -0
  49. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/utils/__init__.py +0 -0
  50. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/__init__.py +0 -0
  51. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/_common.py +0 -0
  52. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/_plot.py +0 -0
  53. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/decision_tree_knn.py +0 -0
  54. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/grad_cam.py +0 -0
  55. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/gradient_input.py +0 -0
  56. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/ice.py +0 -0
  57. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/lime.py +0 -0
  58. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/lrp.py +0 -0
  59. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/pdp.py +0 -0
  60. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/regression.py +0 -0
  61. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/shap.py +0 -0
  62. {dlspit-2.2.0 → dlspit-2.3.0}/dl_experiments/xai/xrai.py +0 -0
  63. {dlspit-2.2.0 → dlspit-2.3.0}/tests/conftest.py +0 -0
  64. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_genai_chatbot_diffusion_music.py +0 -0
  65. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_genai_core.py +0 -0
  66. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_genai_images.py +0 -0
  67. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_genai_mini_gpt.py +0 -0
  68. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_genai_vae_gan.py +0 -0
  69. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_xai_core.py +0 -0
  70. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_xai_neural.py +0 -0
  71. {dlspit-2.2.0 → dlspit-2.3.0}/tests/test_xai_tabular.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dlspit
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A multi-domain experiment package: Deep Learning (MLP, CNN, ResNet, RNN, LSTM, Transformer, GAN, VAE), ML ESE Lab (regression, SVM, decision tree, random forest, XGBoost, k-means) Generative AI from scratch in NumPy (VAE, GAN, mini GPT, text-to-image, face swap, music, CycleGAN, chatbot, diffusion) and Explainable AI from scratch (SHAP, PDP, ICE, LIME, Grad-CAM, XRAI-style, Gradient x Input, LRP).
5
5
  Project-URL: Repository, https://github.com/PrathamShid07/DL_Expts
6
6
  Project-URL: Issues, https://github.com/PrathamShid07/DL_Expts/issues
@@ -43,7 +43,7 @@ Provides-Extra: xai
43
43
  Requires-Dist: matplotlib>=3.6; extra == 'xai'
44
44
  Description-Content-Type: text/markdown
45
45
 
46
- # dlspit v2.2.0 — Multi-Domain Experiment Package
46
+ # dlspit v2.3.0 — Multi-Domain Experiment Package
47
47
 
48
48
  A pip-installable multi-domain experiment viewer and runner covering:
49
49
 
@@ -135,6 +135,52 @@ python -m dl_experiments ML-ESE --run random_forest
135
135
 
136
136
  ---
137
137
 
138
+ ## Read an experiment in Google Colab / Jupyter
139
+
140
+ Install the package in a notebook and call one function. The cell output shows the experiment's **existing theory**
141
+ followed by its **complete Python source code** (syntax-highlighted), exactly as stored in the package. Nothing is executed
142
+ and nothing is rewritten: the theory is the module docstring of the experiment's `.py` file (GenAI, XAI) or its leading
143
+ `# THEORY` comment block (DL, ML-ESE), and the code is the file itself.
144
+
145
+ ```python
146
+ !pip install dlspit
147
+
148
+ from dl_experiments import show_experiment
149
+
150
+ show_experiment("genai", "chatbot") # theory + full source of the chatbot experiment
151
+ show_experiment("xai", "shap") # domains: "genai", "xai", "dl", "ml-ese"
152
+ show_experiment("dl", "4CNN")
153
+ show_experiment("ml-ese", "decision_tree")
154
+ ```
155
+
156
+ Useful options:
157
+
158
+ ```python
159
+ show_experiment("xai", "grad-cam", code=False) # theory only
160
+ show_experiment("xai", "grad-cam", theory=False) # code only
161
+ show_experiment("genai", "vae", helpers=True) # also the supporting modules it imports (e.g. the NumPy toolkit _nn.py)
162
+ show_experiment("xai", "lrp", output="text") # force plain text (default: HTML in a notebook, text in a terminal)
163
+ ```
164
+
165
+ The valid names are listed by `dl_experiments.genai.list_experiments()`, `dl_experiments.xai.list_experiments()`,
166
+ `dl_experiments.dl.list_experiments()` and `dl_experiments.ml_ese.list_experiments()`; the same aliases as the command line work
167
+ (`"gradcam"`, `"text_to_image"`, ...). An invalid name raises an error that lists the valid ones.
168
+
169
+ To get the text itself (for example to copy it into a journal, or to print it):
170
+
171
+ ```python
172
+ from dl_experiments import get_theory, get_source
173
+
174
+ print(get_theory("genai", "chatbot")) # the explanation only
175
+ print(get_source("genai", "chatbot")) # the complete file
176
+ ```
177
+
178
+ Syntax highlighting uses Pygments, which Colab and Jupyter already include (without it the code is shown as plain monospaced
179
+ text); no extra package is required. To *run* an experiment instead of reading it, use `dlspit genai chatbot` or
180
+ `from dl_experiments.genai.chatbot import run_experiment`.
181
+
182
+ ---
183
+
138
184
  ## DL Experiment Index
139
185
 
140
186
  | Key | Description |
@@ -1,4 +1,4 @@
1
- # dlspit v2.2.0 — Multi-Domain Experiment Package
1
+ # dlspit v2.3.0 — Multi-Domain Experiment Package
2
2
 
3
3
  A pip-installable multi-domain experiment viewer and runner covering:
4
4
 
@@ -90,6 +90,52 @@ python -m dl_experiments ML-ESE --run random_forest
90
90
 
91
91
  ---
92
92
 
93
+ ## Read an experiment in Google Colab / Jupyter
94
+
95
+ Install the package in a notebook and call one function. The cell output shows the experiment's **existing theory**
96
+ followed by its **complete Python source code** (syntax-highlighted), exactly as stored in the package. Nothing is executed
97
+ and nothing is rewritten: the theory is the module docstring of the experiment's `.py` file (GenAI, XAI) or its leading
98
+ `# THEORY` comment block (DL, ML-ESE), and the code is the file itself.
99
+
100
+ ```python
101
+ !pip install dlspit
102
+
103
+ from dl_experiments import show_experiment
104
+
105
+ show_experiment("genai", "chatbot") # theory + full source of the chatbot experiment
106
+ show_experiment("xai", "shap") # domains: "genai", "xai", "dl", "ml-ese"
107
+ show_experiment("dl", "4CNN")
108
+ show_experiment("ml-ese", "decision_tree")
109
+ ```
110
+
111
+ Useful options:
112
+
113
+ ```python
114
+ show_experiment("xai", "grad-cam", code=False) # theory only
115
+ show_experiment("xai", "grad-cam", theory=False) # code only
116
+ show_experiment("genai", "vae", helpers=True) # also the supporting modules it imports (e.g. the NumPy toolkit _nn.py)
117
+ show_experiment("xai", "lrp", output="text") # force plain text (default: HTML in a notebook, text in a terminal)
118
+ ```
119
+
120
+ The valid names are listed by `dl_experiments.genai.list_experiments()`, `dl_experiments.xai.list_experiments()`,
121
+ `dl_experiments.dl.list_experiments()` and `dl_experiments.ml_ese.list_experiments()`; the same aliases as the command line work
122
+ (`"gradcam"`, `"text_to_image"`, ...). An invalid name raises an error that lists the valid ones.
123
+
124
+ To get the text itself (for example to copy it into a journal, or to print it):
125
+
126
+ ```python
127
+ from dl_experiments import get_theory, get_source
128
+
129
+ print(get_theory("genai", "chatbot")) # the explanation only
130
+ print(get_source("genai", "chatbot")) # the complete file
131
+ ```
132
+
133
+ Syntax highlighting uses Pygments, which Colab and Jupyter already include (without it the code is shown as plain monospaced
134
+ text); no extra package is required. To *run* an experiment instead of reading it, use `dlspit genai chatbot` or
135
+ `from dl_experiments.genai.chatbot import run_experiment`.
136
+
137
+ ---
138
+
93
139
  ## DL Experiment Index
94
140
 
95
141
  | Key | Description |
@@ -1,5 +1,5 @@
1
1
  """
2
- dl_experiments v2.2.0
2
+ dl_experiments v2.3.0
3
3
  =====================
4
4
  A pip-installable multi-domain experiment package covering:
5
5
 
@@ -37,6 +37,12 @@ Quick start
37
37
  dlspit DL --list # show DL experiments
38
38
  dlspit ML-ESE --list # show ML-ESE experiments
39
39
 
40
+ Notebook / Colab: read the theory and the full code of any experiment
41
+ --------------------------------------------------------------------
42
+ from dl_experiments import show_experiment
43
+ show_experiment("genai", "chatbot") # theory, then the complete Python source (highlighted)
44
+ show_experiment("xai", "shap") # also "dl" and "ml-ese"; nothing is executed
45
+
40
46
  Python API
41
47
  ----------
42
48
  from dl_experiments.dl import list_experiments, show_source, run_experiment
@@ -74,7 +80,7 @@ ML-ESE Experiment keys
74
80
  kmeans_clustering K-Means Clustering — customer segmentation
75
81
  """
76
82
 
77
- __version__ = "2.2.0"
83
+ __version__ = "2.3.0"
78
84
  __author__ = "Pratham"
79
85
 
80
86
  from .dl.registry import (
@@ -99,6 +105,8 @@ from .xai.registry import (
99
105
  show_source as xai_show_source,
100
106
  )
101
107
 
108
+ from .display import get_source, get_theory, show_experiment
109
+
102
110
  __all__ = [
103
111
  "__version__",
104
112
  "__author__",
@@ -114,4 +122,7 @@ __all__ = [
114
122
  "xai_list_experiments",
115
123
  "xai_run_experiment",
116
124
  "xai_show_source",
125
+ "show_experiment",
126
+ "get_theory",
127
+ "get_source",
117
128
  ]
@@ -116,7 +116,7 @@ def cli() -> None:
116
116
  parser = argparse.ArgumentParser(
117
117
  prog="dlspit",
118
118
  description=(
119
- "dlspit v2.2.0 — Multi-Domain Experiment Viewer & Runner\n\n"
119
+ "dlspit v2.3.0 — Multi-Domain Experiment Viewer & Runner\n\n"
120
120
  "Domains:\n"
121
121
  " DL Deep Learning (MLP, CNN, ResNet, RNN, LSTM, GAN, VAE …)\n"
122
122
  " ML-ESE Machine Learning ESE Lab (regression, SVM, trees, clustering …)\n"
@@ -0,0 +1,229 @@
1
+ """
2
+ Show an experiment's THEORY and its complete SOURCE CODE - in Google Colab, Jupyter, or a terminal.
3
+
4
+ from dl_experiments import show_experiment
5
+
6
+ show_experiment("genai", "chatbot") # theory, then the full Python source, highlighted
7
+ show_experiment("xai", "shap")
8
+ show_experiment("dl", "4CNN")
9
+ show_experiment("ml-ese", "decision_tree")
10
+
11
+ Nothing is written or generated here: the text comes from the experiment's own ``.py`` file
12
+ - the module docstring for GenAI / XAI, the leading ``# THEORY`` comment block for DL / ML-ESE -
13
+ and the code is the file itself, character for character. The experiment is NOT imported or
14
+ executed (the file is only read and parsed), so showing it is instant and has no side effects.
15
+
16
+ In a notebook the output is syntax-highlighted HTML (Pygments, which Jupyter and Colab already
17
+ include; without it the code is shown as plain monospaced text). Elsewhere it is printed as text.
18
+
19
+ Public API
20
+ ----------
21
+ show_experiment(domain, name, theory=True, code=True, helpers=False, output="auto")
22
+ get_theory(domain, name) -> str (the explanation text)
23
+ get_source(domain, name, helpers=...) -> str (the file, or the file + supporting modules)
24
+ """
25
+
26
+ import ast
27
+ import html as _html
28
+ import re
29
+ from pathlib import Path
30
+
31
+ _DOMAIN_ALIASES = {
32
+ "genai": "genai", "gen-ai": "genai", "gen_ai": "genai", "generative": "genai",
33
+ "xai": "xai", "explainable": "xai",
34
+ "dl": "dl", "deep": "dl",
35
+ "ml-ese": "ml-ese", "ml_ese": "ml-ese", "mlese": "ml-ese", "ml": "ml-ese",
36
+ }
37
+ DOMAINS = ("genai", "xai", "dl", "ml-ese")
38
+
39
+
40
+ # ── Locating an experiment ───────────────────────────────────────────────────
41
+
42
+ def _resolve(domain, name):
43
+ """Return (canonical domain, canonical key, Path of the experiment's .py file)."""
44
+ canonical = _DOMAIN_ALIASES.get(str(domain).strip().lower())
45
+ if canonical is None:
46
+ raise ValueError(f"Invalid domain: {domain!r}. Valid domains: {', '.join(DOMAINS)}.")
47
+ if canonical == "genai":
48
+ from .genai import registry
49
+ elif canonical == "xai":
50
+ from .xai import registry
51
+ elif canonical == "dl":
52
+ from .dl import registry
53
+ else:
54
+ from .ml_ese import registry
55
+
56
+ if canonical in ("genai", "xai"):
57
+ key = registry.normalize_key(name) # accepts aliases such as "gradcam" or "text_to_image"
58
+ return canonical, key, registry.source_path(key)
59
+ files = registry._SOURCE_FILES # DL / ML-ESE registries keep a key -> file table
60
+ matches = [k for k in files if k.lower() == str(name).strip().lower()]
61
+ if not matches:
62
+ raise registry._invalid_key_error(str(name))
63
+ return canonical, matches[0], (registry._EXPERIMENTS_DIR / files[matches[0]]).resolve()
64
+
65
+
66
+ # ── Extracting the existing theory ───────────────────────────────────────────
67
+
68
+ def _leading_comment_block(source):
69
+ """Theory of DL / ML-ESE files: the first block of consecutive '#' lines (banner lines of '=' dropped)."""
70
+ block, started = [], False
71
+ for line in source.splitlines():
72
+ stripped = line.strip()
73
+ if stripped.startswith("#"):
74
+ started = True
75
+ text = stripped[1:]
76
+ text = text[1:] if text.startswith(" ") else text
77
+ if not re.fullmatch(r"=+", text.strip()):
78
+ block.append(text.rstrip())
79
+ elif started:
80
+ break
81
+ return "\n".join(block).strip()
82
+
83
+
84
+ def _theory_from_source(source):
85
+ docstring = ast.get_docstring(ast.parse(source), clean=True)
86
+ return docstring if docstring else _leading_comment_block(source)
87
+
88
+
89
+ def _title_from_theory(theory, key):
90
+ """First non-empty line: the docstring's title line, or the 'EXPERIMENT n: ...' line of a comment block."""
91
+ return next((line.strip() for line in theory.splitlines() if line.strip()), key)
92
+
93
+
94
+ def get_theory(domain, name):
95
+ """The existing theory / explanation text of an experiment (module docstring or leading comment block)."""
96
+ _, _, path = _resolve(domain, name)
97
+ return _theory_from_source(path.read_text(encoding="utf-8"))
98
+
99
+
100
+ # ── Extracting the code (optionally with the modules it depends on) ──────────
101
+
102
+ def _local_imports(path):
103
+ """Files of this package that ``path`` imports with relative imports (``from ._nn import ...``)."""
104
+ found = []
105
+ for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
106
+ if isinstance(node, ast.ImportFrom) and node.level > 0:
107
+ base = path.parent
108
+ for _ in range(node.level - 1):
109
+ base = base.parent
110
+ parts = node.module.split(".") if node.module else []
111
+ targets = [parts] if parts else [[alias.name] for alias in node.names] # "from . import _plot"
112
+ for target in targets:
113
+ candidate = base.joinpath(*target).with_suffix(".py")
114
+ if candidate.is_file() and candidate.name not in ("registry.py", "__init__.py"):
115
+ found.append(candidate.resolve())
116
+ return found
117
+
118
+
119
+ def _supporting_files(path):
120
+ """Every supporting module the experiment needs, in the order they are first imported (no duplicates)."""
121
+ ordered, queue = [], _local_imports(path)
122
+ while queue:
123
+ current = queue.pop(0)
124
+ if current != path and current not in ordered:
125
+ ordered.append(current)
126
+ queue.extend(_local_imports(current))
127
+ return ordered
128
+
129
+
130
+ def _code_files(domain, name, helpers):
131
+ _, _, path = _resolve(domain, name)
132
+ return [path] + (_supporting_files(path) if helpers else [])
133
+
134
+
135
+ def get_source(domain, name, helpers=False):
136
+ """The experiment's source file, exactly as stored in the package.
137
+
138
+ helpers=True appends the supporting modules it imports (for example the NumPy neural-network
139
+ toolkit ``_nn.py``), each preceded by a comment line naming the file.
140
+ """
141
+ files = _code_files(domain, name, helpers)
142
+ parts = [files[0].read_text(encoding="utf-8")]
143
+ for extra in files[1:]:
144
+ parts.append(f"# {'=' * 70}\n# Supporting module: {extra.name}\n# {'=' * 70}\n" + extra.read_text(encoding="utf-8"))
145
+ return "\n\n".join(parts)
146
+
147
+
148
+ # ── Rendering ────────────────────────────────────────────────────────────────
149
+
150
+ _BOX = ("background:#f6f8fa;color:#24292f;padding:10px 14px;border:1px solid #d0d7de;border-radius:6px;"
151
+ "overflow:auto;font-size:13px;line-height:1.45;")
152
+
153
+
154
+ def _highlight_html(code):
155
+ """Syntax-highlighted HTML with inline styles (no CSS dependencies); plain <pre> without Pygments."""
156
+ try:
157
+ from pygments import highlight
158
+ from pygments.formatters import HtmlFormatter
159
+ from pygments.lexers import PythonLexer
160
+ return highlight(code, PythonLexer(), HtmlFormatter(noclasses=True, style="default", cssstyles=_BOX))
161
+ except ImportError:
162
+ return f'<pre style="{_BOX}">{_html.escape(code)}</pre>'
163
+
164
+
165
+ def _build_html(title, theory, code_files, show_theory, show_code):
166
+ out = [f'<div style="font-family:system-ui,sans-serif"><h2 style="margin-bottom:4px">{_html.escape(title)}</h2>']
167
+ if show_theory:
168
+ out.append('<h3>Theory and Underlying Concepts</h3>'
169
+ f'<pre style="{_BOX}white-space:pre-wrap">{_html.escape(theory)}</pre>')
170
+ if show_code:
171
+ out.append("<h3>Python Implementation</h3>")
172
+ for path, source in code_files:
173
+ out.append(f'<div style="margin:6px 0 2px;color:#57606a;font-size:12px">{_html.escape(path.name)}</div>'
174
+ + _highlight_html(source))
175
+ out.append("</div>")
176
+ return "".join(out)
177
+
178
+
179
+ def _display_html(markup):
180
+ """Send HTML to the notebook front-end (separate function so tests can intercept it)."""
181
+ from IPython.display import HTML, display
182
+ display(HTML(markup))
183
+
184
+
185
+ def _in_notebook():
186
+ """True inside Jupyter, Colab or VS Code notebooks (an IPython kernel), False in a terminal / script."""
187
+ try:
188
+ from IPython import get_ipython
189
+ except ImportError:
190
+ return False
191
+ shell = get_ipython()
192
+ return shell is not None and hasattr(shell, "kernel")
193
+
194
+
195
+ def show_experiment(domain, name, theory=True, code=True, helpers=False, output="auto"):
196
+ """Display an experiment's existing theory and its complete source code. Nothing is executed.
197
+
198
+ Parameters
199
+ ----------
200
+ domain : "genai", "xai", "dl" or "ml-ese"
201
+ name : experiment key, e.g. "chatbot", "shap", "grad-cam", "4CNN", "decision_tree"
202
+ (``dl_experiments.genai.list_experiments()`` etc. list the valid names)
203
+ theory : show the "Theory and Underlying Concepts" section
204
+ code : show the "Python Implementation" section
205
+ helpers : also show the supporting modules the experiment imports (e.g. the ``_nn.py`` toolkit)
206
+ output : "auto" (HTML in a notebook, text elsewhere), "html" or "text"
207
+ """
208
+ if not theory and not code:
209
+ raise ValueError("Nothing to show: at least one of theory / code must be True.")
210
+ if output not in ("auto", "html", "text"):
211
+ raise ValueError("output must be 'auto', 'html' or 'text'.")
212
+ _, key, path = _resolve(domain, name)
213
+ files = _code_files(domain, name, helpers)
214
+ sources = [(f, f.read_text(encoding="utf-8")) for f in files]
215
+ theory_text = _theory_from_source(sources[0][1])
216
+ title = _title_from_theory(theory_text, key)
217
+
218
+ use_html = output == "html" or (output == "auto" and _in_notebook())
219
+ if use_html:
220
+ _display_html(_build_html(title, theory_text, sources, theory, code))
221
+ return None
222
+ bar = "=" * 72
223
+ print(f"{bar}\n{title}\n{bar}")
224
+ if theory:
225
+ print(f"\n--- Theory and Underlying Concepts {'-' * 38}\n\n{theory_text}\n")
226
+ if code:
227
+ for f, source in sources:
228
+ print(f"\n--- Python Implementation: {f.name} {'-' * max(3, 46 - len(f.name))}\n\n{source.rstrip()}\n")
229
+ return None
@@ -90,6 +90,11 @@ def run_experiment(key, **options):
90
90
  return function(**options)
91
91
 
92
92
 
93
+ def source_path(key):
94
+ """Path of the .py file that implements ``key`` (used by ``dl_experiments.show_experiment``)."""
95
+ return (_EXPERIMENTS_DIR / f"{_EXPERIMENTS[normalize_key(key)][1]}.py").resolve()
96
+
97
+
93
98
  def show_source(key):
94
99
  """Print the source file of an experiment (the code is not executed)."""
95
100
  key = normalize_key(key)
@@ -88,6 +88,11 @@ def run_experiment(key, **options):
88
88
  return function(**options)
89
89
 
90
90
 
91
+ def source_path(key):
92
+ """Path of the .py file that implements ``key`` (used by ``dl_experiments.show_experiment``)."""
93
+ return (_EXPERIMENTS_DIR / f"{_EXPERIMENTS[normalize_key(key)][1]}.py").resolve()
94
+
95
+
91
96
  def show_source(key):
92
97
  """Print the source file of an experiment (the code is not executed)."""
93
98
  key = normalize_key(key)
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  # ── Project metadata ────────────────────────────────────────────────────────
6
6
  [project]
7
7
  name = "dlspit" # UNIQUE name on PyPI
8
- version = "2.2.0"
8
+ version = "2.3.0"
9
9
  description = "A multi-domain experiment package: Deep Learning (MLP, CNN, ResNet, RNN, LSTM, Transformer, GAN, VAE), ML ESE Lab (regression, SVM, decision tree, random forest, XGBoost, k-means) Generative AI from scratch in NumPy (VAE, GAN, mini GPT, text-to-image, face swap, music, CycleGAN, chatbot, diffusion) and Explainable AI from scratch (SHAP, PDP, ICE, LIME, Grad-CAM, XRAI-style, Gradient x Input, LRP)."
10
10
  readme = "README.md"
11
11
  license = { text = "MIT" }
@@ -0,0 +1,245 @@
1
+ """Tests for ``show_experiment`` / ``get_theory`` / ``get_source`` (theory + code display for notebooks)."""
2
+
3
+ import ast
4
+ import html
5
+ import importlib.util
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ import types
10
+
11
+ import pytest
12
+
13
+ import dl_experiments
14
+ from conftest import subprocess_env
15
+ from dl_experiments import display, get_source, get_theory, show_experiment
16
+ from dl_experiments.dl import registry as dl_registry
17
+ from dl_experiments.genai import registry as genai_registry
18
+ from dl_experiments.ml_ese import registry as ml_registry
19
+ from dl_experiments.xai import registry as xai_registry
20
+
21
+ ALL = ([("genai", k, genai_registry.source_path(k)) for k in genai_registry.list_experiments()]
22
+ + [("xai", k, xai_registry.source_path(k)) for k in xai_registry.list_experiments()]
23
+ + [("dl", k, dl_registry._EXPERIMENTS_DIR / f) for k, f in dl_registry._SOURCE_FILES.items()]
24
+ + [("ml-ese", k, ml_registry._EXPERIMENTS_DIR / f) for k, f in ml_registry._SOURCE_FILES.items()])
25
+ has_pygments = importlib.util.find_spec("pygments") is not None
26
+
27
+
28
+ def strip_markup(markup):
29
+ """Visible text of an HTML fragment (tags removed, entities decoded)."""
30
+ return html.unescape(re.sub(r"<[^>]+>", "", markup))
31
+
32
+
33
+ @pytest.fixture
34
+ def captured(monkeypatch):
35
+ """Intercept what would be sent to the notebook front-end."""
36
+ shown = []
37
+ monkeypatch.setattr(display, "_display_html", shown.append)
38
+ return shown
39
+
40
+
41
+ # ── theory extraction ────────────────────────────────────────────────────────
42
+
43
+ def test_every_experiment_has_theory_and_source():
44
+ assert len(ALL) == 9 + 10 + 13 + 8
45
+ for domain, key, path in ALL:
46
+ assert get_theory(domain, key).strip(), (domain, key)
47
+ assert get_source(domain, key) == path.read_text(encoding="utf-8"), (domain, key) # verbatim file content
48
+
49
+
50
+ @pytest.mark.parametrize("domain,key,path", [a for a in ALL if a[0] in ("genai", "xai")])
51
+ def test_genai_and_xai_theory_is_the_module_docstring(domain, key, path):
52
+ assert get_theory(domain, key) == ast.get_docstring(ast.parse(path.read_text(encoding="utf-8")), clean=True)
53
+
54
+
55
+ def test_dl_and_mlese_theory_is_the_leading_comment_block():
56
+ text = get_theory("dl", "4CNN")
57
+ assert text.startswith("EXPERIMENT 4: Convolutional Neural Network (CNN) on MNIST") and "THEORY:" in text
58
+ assert "=====" not in text and not text.startswith("#") # banner and '# ' prefixes removed
59
+ assert "Decision Tree" in get_theory("ml-ese", "decision_tree").splitlines()[0]
60
+
61
+
62
+ def test_theory_text_is_the_real_existing_explanation():
63
+ chatbot = get_theory("genai", "chatbot")
64
+ assert "this is NOT a generative LLM" in chatbot and "bag-of-words" in chatbot
65
+ shap = get_theory("xai", "shap")
66
+ assert "phi_i" in shap and "Limitations" in shap and "background" in shap
67
+ assert get_theory("xai", "lrp") == get_theory("XAI", "LRP") # domain / name are case-insensitive
68
+
69
+
70
+ @pytest.mark.parametrize("domain,name,expected_file", [("GenAI", "text_to_image", "text_to_image.py"), ("xai", "gradcam", "grad_cam.py"),
71
+ ("xai", "7", "grad_cam.py"), ("genai", "jukebox", "music_generation.py"),
72
+ ("DL", "4cnn", "exp4_cnn.py"), ("ml_ese", "SVM_Classifier", "svm_classifier.py")])
73
+ def test_aliases_resolve_to_the_right_file(domain, name, expected_file):
74
+ assert display._resolve(domain, name)[2].name == expected_file
75
+
76
+
77
+ # ── invalid input ────────────────────────────────────────────────────────────
78
+
79
+ def test_invalid_domain_and_experiment_names():
80
+ with pytest.raises(ValueError, match="Invalid domain"):
81
+ show_experiment("nlp", "bert")
82
+ with pytest.raises(ValueError, match="Did you mean"):
83
+ show_experiment("xai", "shapp")
84
+ with pytest.raises(ValueError, match="Invalid XAI"):
85
+ get_theory("xai", "vae") # a GenAI name is not an XAI name
86
+ with pytest.raises(ValueError, match="Invalid experiment key"):
87
+ get_source("dl", "99Nothing")
88
+ with pytest.raises(ValueError, match="Invalid experiment key"):
89
+ get_theory("ml-ese", "unknown_model")
90
+
91
+
92
+ def test_invalid_options():
93
+ with pytest.raises(ValueError, match="at least one"):
94
+ show_experiment("xai", "pdp", theory=False, code=False)
95
+ with pytest.raises(ValueError, match="output"):
96
+ show_experiment("xai", "pdp", output="pdf")
97
+
98
+
99
+ # ── text output (terminal / script) ──────────────────────────────────────────
100
+
101
+ def test_text_output_has_title_then_theory_then_the_complete_source(capsys):
102
+ show_experiment("genai", "diffusion", output="text")
103
+ out = capsys.readouterr().out
104
+ source = get_source("genai", "diffusion")
105
+ theory = get_theory("genai", "diffusion")
106
+ assert out.index("Theory and Underlying Concepts") < out.index("Python Implementation")
107
+ assert theory in out and source.rstrip() in out # the complete file, unchanged
108
+ assert out.startswith("=" * 72 + "\nGenAI Experiment 9 - Denoising Diffusion")
109
+
110
+
111
+ def test_theory_only_and_code_only(capsys):
112
+ show_experiment("xai", "ice", code=False, output="text")
113
+ only_theory = capsys.readouterr().out
114
+ assert "Theory and Underlying Concepts" in only_theory and "Python Implementation" not in only_theory
115
+ assert "def calculate_ice" not in only_theory
116
+ show_experiment("xai", "ice", theory=False, output="text")
117
+ only_code = capsys.readouterr().out
118
+ assert "def calculate_ice" in only_code and "Theory and Underlying Concepts" not in only_code
119
+
120
+
121
+ def test_auto_mode_prints_text_outside_a_notebook(capsys, captured):
122
+ assert show_experiment("xai", "lime") is None # returns nothing (no double display)
123
+ assert "Python Implementation" in capsys.readouterr().out and captured == []
124
+
125
+
126
+ def test_works_without_ipython_and_pygments_installed():
127
+ code = ("import sys\nsys.modules['IPython'] = None\nsys.modules['pygments'] = None\n"
128
+ "from dl_experiments import show_experiment\n"
129
+ "show_experiment('xai', 'pdp', output='text')\n"
130
+ "import dl_experiments.display as d\nd._display_html = lambda m: print('HTML-LEN', len(m), '<pre' in m)\n"
131
+ "show_experiment('xai', 'pdp', output='html')\n")
132
+ r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, env=subprocess_env(), timeout=60)
133
+ assert r.returncode == 0, r.stderr
134
+ assert "def calculate_pdp" in r.stdout and "HTML-LEN" in r.stdout and "True" in r.stdout # plain <pre> fallback
135
+
136
+
137
+ # ── HTML output (notebook / Colab) ───────────────────────────────────────────
138
+
139
+ def test_html_output_contains_title_theory_and_the_unchanged_source(captured):
140
+ show_experiment("xai", "shap", output="html")
141
+ assert len(captured) == 1
142
+ markup = captured[0]
143
+ visible = strip_markup(markup)
144
+ assert "Theory and Underlying Concepts" in markup and "Python Implementation" in markup
145
+ assert markup.index("Theory and Underlying Concepts") < markup.index("Python Implementation")
146
+ assert get_theory("xai", "shap") in visible # theory shown unchanged
147
+ assert get_source("xai", "shap").rstrip("\n") in visible # the source, character for character
148
+ assert "<h2" in markup and "XAI Experiment 3" in visible
149
+
150
+
151
+ @pytest.mark.skipif(not has_pygments, reason="pygments not installed")
152
+ def test_html_code_is_syntax_highlighted_with_inline_styles(captured):
153
+ show_experiment("genai", "gan", output="html")
154
+ markup = captured[0]
155
+ assert re.search(r'<span style="[^"]*font-weight: bold[^"]*">def</span>', markup) # keyword styled
156
+ assert re.search(r'<span style="[^"]*font-style: italic[^"]*"># ', markup) # comment styled
157
+ assert "<style" not in markup and "<link" not in markup and "<script" not in markup # inline styles only
158
+ assert display._highlight_html("x = 1 < 2\n").count("&lt;") == 1
159
+
160
+
161
+ def test_html_is_escaped_so_code_cannot_break_the_page(captured):
162
+ show_experiment("genai", "vae", output="html") # source contains '<', '>' and '&' characters
163
+ markup = captured[0]
164
+ assert "<script" not in markup.lower()
165
+ assert "&lt;" in markup or "&gt;" in markup
166
+ assert strip_markup(markup).count("def ") >= get_source("genai", "vae").count("def ")
167
+
168
+
169
+ def test_html_shows_only_what_was_requested(captured):
170
+ show_experiment("dl", "4CNN", code=False, output="html")
171
+ assert "Theory and Underlying Concepts" in captured[0] and "Python Implementation" not in captured[0]
172
+ show_experiment("dl", "4CNN", theory=False, output="html")
173
+ assert "Python Implementation" in captured[1] and "Theory and Underlying Concepts" not in captured[1]
174
+
175
+
176
+ def test_notebook_is_detected_and_ipython_display_is_used(monkeypatch):
177
+ shown = []
178
+ ipython = types.ModuleType("IPython")
179
+ ipython.get_ipython = lambda: types.SimpleNamespace(kernel=object()) # what Jupyter / Colab provide
180
+ display_mod = types.ModuleType("IPython.display")
181
+ display_mod.HTML = lambda markup: ("HTML", markup)
182
+ display_mod.display = shown.append
183
+ monkeypatch.setitem(sys.modules, "IPython", ipython)
184
+ monkeypatch.setitem(sys.modules, "IPython.display", display_mod)
185
+ assert display._in_notebook() is True
186
+ show_experiment("xai", "regression") # output="auto" -> HTML in a notebook
187
+ assert len(shown) == 1 and shown[0][0] == "HTML" and "Theory and Underlying Concepts" in shown[0][1]
188
+ ipython.get_ipython = lambda: types.SimpleNamespace() # a terminal IPython has no kernel
189
+ assert display._in_notebook() is False
190
+
191
+
192
+ # ── helpers (supporting modules) ─────────────────────────────────────────────
193
+
194
+ def test_supporting_modules_are_optional_and_resolved_from_the_real_imports():
195
+ plain = get_source("xai", "ice")
196
+ with_helpers = get_source("xai", "ice", helpers=True)
197
+ assert with_helpers.startswith(plain) and len(with_helpers) > len(plain)
198
+ names = re.findall(r"# Supporting module: (\S+)", with_helpers)
199
+ assert names[:1] == ["_nn.py"] and {"_common.py", "pdp.py"} <= set(names) and len(names) == len(set(names))
200
+ assert "Supporting module" not in plain and "class Dense" not in plain and "class Dense" in with_helpers
201
+ assert "run_experiment" in with_helpers and with_helpers.count("def calculate_pdp") == 1
202
+
203
+
204
+ def test_helpers_for_experiments_without_dependencies(capsys):
205
+ assert get_source("dl", "4CNN", helpers=True) == get_source("dl", "4CNN") # DL files import nothing local
206
+ show_experiment("genai", "vae", theory=False, helpers=True, output="text")
207
+ out = capsys.readouterr().out
208
+ assert "Python Implementation: vae.py" in out and "Python Implementation: _nn.py" in out and "Python Implementation: _viz.py" in out
209
+
210
+
211
+ # ── showing an experiment never runs it ──────────────────────────────────────
212
+
213
+ def test_showing_does_not_import_or_execute_the_experiment():
214
+ code = ("import sys, time\nfrom dl_experiments import show_experiment\n"
215
+ "t = time.time()\n"
216
+ "for d, n in (('genai','diffusion'), ('genai','cyclegan'), ('xai','grad-cam'), ('xai','lrp')):\n"
217
+ " show_experiment(d, n, output='text')\n"
218
+ "bad = [m for m in ('dl_experiments.genai.diffusion','dl_experiments.genai.cyclegan','dl_experiments.xai.grad_cam','dl_experiments.xai.lrp') if m in sys.modules]\n"
219
+ "print('IMPORTED:', bad, 'SECONDS<2:', time.time() - t < 2)\n")
220
+ r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, env=subprocess_env(), timeout=60)
221
+ assert r.returncode == 0, r.stderr
222
+ assert "IMPORTED: [] SECONDS<2: True" in r.stdout
223
+ assert "Chamfer distance to real data: 0." not in r.stdout # no experiment output appeared
224
+
225
+
226
+ def test_public_names_are_exported():
227
+ for name in ("show_experiment", "get_theory", "get_source"):
228
+ assert name in dl_experiments.__all__ and callable(getattr(dl_experiments, name))
229
+
230
+
231
+ # ── a real Jupyter kernel (only when nbclient + ipykernel are installed) ─────
232
+
233
+ @pytest.mark.skipif(any(importlib.util.find_spec(m) is None for m in ("nbclient", "nbformat", "ipykernel")),
234
+ reason="needs nbclient, nbformat and ipykernel")
235
+ def test_real_notebook_cell_renders_theory_and_code_as_html(tmp_path):
236
+ import nbformat
237
+ from nbclient import NotebookClient
238
+ nb = nbformat.v4.new_notebook(cells=[nbformat.v4.new_code_cell(
239
+ "from dl_experiments import show_experiment\nshow_experiment('genai', 'chatbot')")])
240
+ NotebookClient(nb, timeout=120, kernel_name="python3", resources={"metadata": {"path": str(tmp_path)}}).execute()
241
+ outputs = nb.cells[0].outputs
242
+ assert len(outputs) == 1 and outputs[0].output_type == "display_data" and "text/html" in outputs[0].data
243
+ visible = strip_markup(outputs[0].data["text/html"])
244
+ assert "Theory and Underlying Concepts" in visible and get_source("genai", "chatbot").rstrip("\n") in visible
245
+ assert "text/plain" in outputs[0].data
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes