jupytermind 0.3.0

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 (193) hide show
  1. package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
  2. package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
  3. package/.github/skills/ai-data-scientist/SKILL.md +330 -0
  4. package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
  5. package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
  6. package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
  7. package/.github/skills/ai-materials-scientist/manifest.json +58 -0
  8. package/.github/skills/ai-scientist/SKILL.md +69 -0
  9. package/.github/skills/ai-scientist/manifest.json +61 -0
  10. package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
  11. package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
  12. package/.github/skills/japanese-prose/NOTICE.md +17 -0
  13. package/.github/skills/japanese-prose/SKILL.md +111 -0
  14. package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
  15. package/.github/skills/japanese-prose/references/scoring.md +24 -0
  16. package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
  17. package/.github/skills/japanese-prose/scripts/core.py +192 -0
  18. package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
  19. package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
  20. package/.github/skills/japanese-prose/scripts/lint.py +378 -0
  21. package/.github/skills/japanese-prose/scripts/outline.py +68 -0
  22. package/.github/skills/japanese-prose/scripts/terms.py +112 -0
  23. package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
  24. package/.github/skills/presentation-planner/SKILL.md +257 -0
  25. package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
  26. package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
  27. package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
  28. package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
  29. package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
  30. package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
  31. package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
  32. package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
  33. package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
  34. package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
  35. package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
  36. package/.github/skills/tech-writer/SKILL.md +434 -0
  37. package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
  38. package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
  39. package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
  40. package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
  41. package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
  42. package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
  43. package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
  44. package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
  45. package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
  46. package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
  47. package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
  48. package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
  49. package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
  50. package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
  51. package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
  52. package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
  53. package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
  54. package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
  55. package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
  56. package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
  57. package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
  58. package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
  59. package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
  60. package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
  61. package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
  62. package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
  63. package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
  64. package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
  65. package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
  66. package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
  67. package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
  68. package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
  69. package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
  70. package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
  71. package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
  72. package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
  73. package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
  74. package/.github/skills/tech-writer/references/style-constitution.md +104 -0
  75. package/.github/skills/tech-writer/scripts/lint.py +412 -0
  76. package/LICENSE +21 -0
  77. package/README.md +92 -0
  78. package/bin/ai-data-scientist.js +123 -0
  79. package/package.json +41 -0
  80. package/pyproject.toml +45 -0
  81. package/src/ai_chemistry_scientist/__init__.py +0 -0
  82. package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
  83. package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
  84. package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
  85. package/src/ai_chemistry_scientist/dispatch.py +369 -0
  86. package/src/ai_chemistry_scientist/docking_score.py +97 -0
  87. package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
  88. package/src/ai_chemistry_scientist/evidence.py +41 -0
  89. package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
  90. package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
  91. package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
  92. package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
  93. package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
  94. package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
  95. package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
  96. package/src/ai_chemistry_scientist/validation.py +70 -0
  97. package/src/ai_data_scientist/__init__.py +0 -0
  98. package/src/ai_data_scientist/analysis_assumptions.py +121 -0
  99. package/src/ai_data_scientist/anomaly_detection.py +39 -0
  100. package/src/ai_data_scientist/automl.py +109 -0
  101. package/src/ai_data_scientist/cleaning.py +56 -0
  102. package/src/ai_data_scientist/cli.py +90 -0
  103. package/src/ai_data_scientist/clustering.py +54 -0
  104. package/src/ai_data_scientist/dashboard.py +33 -0
  105. package/src/ai_data_scientist/data_definition.py +100 -0
  106. package/src/ai_data_scientist/data_quality.py +164 -0
  107. package/src/ai_data_scientist/dataset_validation.py +135 -0
  108. package/src/ai_data_scientist/dependency_pins.py +60 -0
  109. package/src/ai_data_scientist/eda.py +82 -0
  110. package/src/ai_data_scientist/experiment_evaluation.py +635 -0
  111. package/src/ai_data_scientist/explainability.py +340 -0
  112. package/src/ai_data_scientist/feature_engineering.py +163 -0
  113. package/src/ai_data_scientist/gate_config.py +32 -0
  114. package/src/ai_data_scientist/ingestion.py +127 -0
  115. package/src/ai_data_scientist/insight_engine.py +180 -0
  116. package/src/ai_data_scientist/japanese_nlp.py +43 -0
  117. package/src/ai_data_scientist/jupyter_launcher.py +137 -0
  118. package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
  119. package/src/ai_data_scientist/language_router.py +28 -0
  120. package/src/ai_data_scientist/lifecycle.py +221 -0
  121. package/src/ai_data_scientist/mcp_gateway.py +113 -0
  122. package/src/ai_data_scientist/mcp_runtime.py +194 -0
  123. package/src/ai_data_scientist/mcp_transport.py +53 -0
  124. package/src/ai_data_scientist/ml_modeling.py +451 -0
  125. package/src/ai_data_scientist/model_tuning.py +104 -0
  126. package/src/ai_data_scientist/notebook_audit.py +574 -0
  127. package/src/ai_data_scientist/project_manager.py +243 -0
  128. package/src/ai_data_scientist/report_export.py +73 -0
  129. package/src/ai_data_scientist/sensitivity.py +445 -0
  130. package/src/ai_data_scientist/signal_analysis.py +201 -0
  131. package/src/ai_data_scientist/skill_packaging.py +40 -0
  132. package/src/ai_data_scientist/stats_analysis.py +88 -0
  133. package/src/ai_data_scientist/text_nlp.py +44 -0
  134. package/src/ai_data_scientist/timeseries.py +68 -0
  135. package/src/ai_data_scientist/visualization.py +708 -0
  136. package/src/ai_genomics_scientist/__init__.py +1 -0
  137. package/src/ai_genomics_scientist/differential_expression.py +147 -0
  138. package/src/ai_genomics_scientist/dispatch.py +267 -0
  139. package/src/ai_genomics_scientist/evidence.py +45 -0
  140. package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
  141. package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
  142. package/src/ai_genomics_scientist/sequence_features.py +111 -0
  143. package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
  144. package/src/ai_genomics_scientist/validation.py +83 -0
  145. package/src/ai_genomics_scientist/variant_effect.py +147 -0
  146. package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
  147. package/src/ai_materials_scientist/__init__.py +0 -0
  148. package/src/ai_materials_scientist/calphad.py +117 -0
  149. package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
  150. package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
  151. package/src/ai_materials_scientist/dispatch.py +100 -0
  152. package/src/ai_materials_scientist/evidence.py +84 -0
  153. package/src/ai_materials_scientist/fem.py +279 -0
  154. package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
  155. package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
  156. package/src/ai_materials_scientist/phase_field.py +167 -0
  157. package/src/ai_materials_scientist/validation.py +70 -0
  158. package/src/ai_scientist/__init__.py +1 -0
  159. package/src/ai_scientist/completion_gate.py +15 -0
  160. package/src/ai_scientist/data_analysis.py +46 -0
  161. package/src/ai_scientist/evidence_registry.py +99 -0
  162. package/src/ai_scientist/experimental_design.py +20 -0
  163. package/src/ai_scientist/language.py +14 -0
  164. package/src/ai_scientist/latex_renderer.py +41 -0
  165. package/src/ai_scientist/literature_review.py +37 -0
  166. package/src/ai_scientist/manifest.py +87 -0
  167. package/src/ai_scientist/manuscript.py +94 -0
  168. package/src/ai_scientist/mcp_config.py +76 -0
  169. package/src/ai_scientist/mcp_external.py +42 -0
  170. package/src/ai_scientist/mcp_failures.py +23 -0
  171. package/src/ai_scientist/mcp_gateway.py +38 -0
  172. package/src/ai_scientist/mcp_managed.py +180 -0
  173. package/src/ai_scientist/npm_packaging.py +49 -0
  174. package/src/ai_scientist/orchestrator.py +133 -0
  175. package/src/ai_scientist/peer_review.py +60 -0
  176. package/src/ai_scientist/phase_gate.py +74 -0
  177. package/src/ai_scientist/phase_state.py +230 -0
  178. package/src/ai_scientist/presentation.py +56 -0
  179. package/src/ai_scientist/project_config.py +31 -0
  180. package/src/ai_scientist/project_handle.py +74 -0
  181. package/src/ai_scientist/reproducibility.py +20 -0
  182. package/src/ai_scientist/research_planning.py +20 -0
  183. package/src/ai_scientist/skill_invocation.py +21 -0
  184. package/src/ai_scientist/tdd_gate.py +99 -0
  185. package/src/ai_structural_biology_scientist/__init__.py +0 -0
  186. package/src/ai_structural_biology_scientist/contact_map.py +87 -0
  187. package/src/ai_structural_biology_scientist/dispatch.py +269 -0
  188. package/src/ai_structural_biology_scientist/evidence.py +43 -0
  189. package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
  190. package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
  191. package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
  192. package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
  193. package/src/ai_structural_biology_scientist/validation.py +100 -0
@@ -0,0 +1,445 @@
1
+ """Reusable sensitivity-analysis plans for conclusion-stability testing.
2
+
3
+ Implements DES-AIDS-044 (REQ-AIDS-056): runs an analysis function
4
+ across a grid of alternative specifications (model/parameter/subset
5
+ choices) for a named target claim and reports whether the conclusion
6
+ direction/magnitude is stable, bounded by an explicit evaluation budget.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import itertools
12
+ import math
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass, field
15
+ from typing import Any
16
+
17
+
18
+ class SensitivityBudgetExceededError(RuntimeError):
19
+ """Raised when a specification grid would exceed the run budget."""
20
+
21
+
22
+ _UNSET = object()
23
+
24
+
25
+ def _validate_target_claim(target_claim: str) -> None:
26
+ if not isinstance(target_claim, str):
27
+ raise TypeError("target_claim must be a string.")
28
+ if not target_claim.strip():
29
+ raise ValueError("target_claim must be a non-empty string.")
30
+
31
+
32
+ # @id CODE-AIDS-079
33
+ # @implements REQ-AIDS-056
34
+ # @design DES-AIDS-044
35
+ @dataclass(frozen=True, init=False)
36
+ class SensitivityPlan:
37
+ """A grid of alternative specifications to re-run an analysis under."""
38
+
39
+ target_claim: str
40
+ parameter_grid: dict[str, list[Any]]
41
+ max_runs: int = 100
42
+
43
+ def __init__(
44
+ self,
45
+ *args: Any,
46
+ target_claim: Any = _UNSET,
47
+ parameter_grid: Any = _UNSET,
48
+ max_runs: Any = _UNSET,
49
+ ) -> None:
50
+ if args and isinstance(args[0], str):
51
+ if len(args) > 3:
52
+ raise TypeError("SensitivityPlan accepts at most 3 positional arguments.")
53
+ if target_claim is not _UNSET:
54
+ raise TypeError("SensitivityPlan got multiple values for target_claim.")
55
+ if len(args) > 1 and parameter_grid is not _UNSET:
56
+ raise TypeError("SensitivityPlan got multiple values for parameter_grid.")
57
+ if len(args) > 2 and max_runs is not _UNSET:
58
+ raise TypeError("SensitivityPlan got multiple values for max_runs.")
59
+ target_claim = args[0]
60
+ if len(args) > 1:
61
+ parameter_grid = args[1]
62
+ if len(args) > 2:
63
+ max_runs = args[2]
64
+ elif args:
65
+ if len(args) > 3:
66
+ raise TypeError("SensitivityPlan accepts at most 3 positional arguments.")
67
+ if parameter_grid is not _UNSET:
68
+ raise TypeError("SensitivityPlan got multiple values for parameter_grid.")
69
+ if len(args) > 1 and max_runs is not _UNSET:
70
+ raise TypeError("SensitivityPlan got multiple values for max_runs.")
71
+ if len(args) > 2 and target_claim is not _UNSET:
72
+ raise TypeError("SensitivityPlan got multiple values for target_claim.")
73
+ parameter_grid = args[0]
74
+ if len(args) > 1:
75
+ max_runs = args[1]
76
+ if len(args) > 2:
77
+ target_claim = args[2]
78
+ if target_claim is _UNSET:
79
+ raise TypeError("SensitivityPlan requires target_claim.")
80
+ if parameter_grid is _UNSET:
81
+ raise TypeError("SensitivityPlan requires parameter_grid.")
82
+ if max_runs is _UNSET:
83
+ max_runs = 100
84
+ object.__setattr__(self, "target_claim", target_claim)
85
+ object.__setattr__(self, "parameter_grid", parameter_grid)
86
+ object.__setattr__(self, "max_runs", max_runs)
87
+ _validate_target_claim(self.target_claim)
88
+
89
+ def specifications(self) -> tuple[dict[str, Any], ...]:
90
+ """Enumerate the Cartesian product of ``parameter_grid`` values."""
91
+ if not self.parameter_grid:
92
+ return ({},)
93
+ keys = list(self.parameter_grid.keys())
94
+ combos = itertools.product(*(self.parameter_grid[k] for k in keys))
95
+ specs = tuple(dict(zip(keys, combo, strict=True)) for combo in combos)
96
+ if len(specs) > self.max_runs:
97
+ raise SensitivityBudgetExceededError(
98
+ f"Specification grid has {len(specs)} combinations, "
99
+ f"exceeding max_runs={self.max_runs}."
100
+ )
101
+ return specs
102
+
103
+
104
+ @dataclass(frozen=True, init=False)
105
+ class SensitivityResult:
106
+ """Outcome of one specification run plus the overall stability verdict."""
107
+
108
+ target_claim: str
109
+ specification: dict[str, Any]
110
+ value: float
111
+ # GitHub #53 / REQ-AIDS-056: an evaluator failure is recorded as a
112
+ # failed specification instead of aborting the remaining plan.
113
+ failed: bool = False
114
+ error: str | None = None
115
+
116
+ def __init__(
117
+ self,
118
+ *args: Any,
119
+ target_claim: Any = _UNSET,
120
+ specification: Any = _UNSET,
121
+ value: Any = _UNSET,
122
+ failed: Any = _UNSET,
123
+ error: Any = _UNSET,
124
+ ) -> None:
125
+ if args and isinstance(args[0], str):
126
+ if len(args) > 5:
127
+ raise TypeError("SensitivityResult accepts at most 5 positional arguments.")
128
+ if target_claim is not _UNSET:
129
+ raise TypeError("SensitivityResult got multiple values for target_claim.")
130
+ if len(args) > 1 and specification is not _UNSET:
131
+ raise TypeError("SensitivityResult got multiple values for specification.")
132
+ if len(args) > 2 and value is not _UNSET:
133
+ raise TypeError("SensitivityResult got multiple values for value.")
134
+ if len(args) > 3 and failed is not _UNSET:
135
+ raise TypeError("SensitivityResult got multiple values for failed.")
136
+ if len(args) > 4 and error is not _UNSET:
137
+ raise TypeError("SensitivityResult got multiple values for error.")
138
+ target_claim = args[0]
139
+ if len(args) > 1:
140
+ specification = args[1]
141
+ if len(args) > 2:
142
+ value = args[2]
143
+ if len(args) > 3:
144
+ failed = args[3]
145
+ if len(args) > 4:
146
+ error = args[4]
147
+ elif args:
148
+ if len(args) > 5:
149
+ raise TypeError("SensitivityResult accepts at most 5 positional arguments.")
150
+ if specification is not _UNSET:
151
+ raise TypeError("SensitivityResult got multiple values for specification.")
152
+ if len(args) > 1 and value is not _UNSET:
153
+ raise TypeError("SensitivityResult got multiple values for value.")
154
+ if len(args) > 2 and failed is not _UNSET:
155
+ raise TypeError("SensitivityResult got multiple values for failed.")
156
+ if len(args) > 3 and error is not _UNSET:
157
+ raise TypeError("SensitivityResult got multiple values for error.")
158
+ if len(args) > 4 and target_claim is not _UNSET:
159
+ raise TypeError("SensitivityResult got multiple values for target_claim.")
160
+ specification = args[0]
161
+ if len(args) > 1:
162
+ value = args[1]
163
+ if len(args) > 2:
164
+ failed = args[2]
165
+ if len(args) > 3:
166
+ error = args[3]
167
+ if len(args) > 4:
168
+ target_claim = args[4]
169
+ if target_claim is _UNSET:
170
+ raise TypeError("SensitivityResult requires target_claim.")
171
+ if specification is _UNSET:
172
+ raise TypeError("SensitivityResult requires specification.")
173
+ if value is _UNSET:
174
+ raise TypeError("SensitivityResult requires value.")
175
+ if failed is _UNSET:
176
+ failed = False
177
+ if error is _UNSET:
178
+ error = None
179
+ object.__setattr__(self, "target_claim", target_claim)
180
+ object.__setattr__(self, "specification", specification)
181
+ object.__setattr__(self, "value", value)
182
+ object.__setattr__(self, "failed", failed)
183
+ object.__setattr__(self, "error", error)
184
+ _validate_target_claim(self.target_claim)
185
+
186
+
187
+ @dataclass(frozen=True, init=False)
188
+ class SensitivityReport:
189
+ """Aggregated result of running a plan: all outcomes and a stability verdict."""
190
+
191
+ target_claim: str
192
+ results: tuple[SensitivityResult, ...] = field(default_factory=tuple)
193
+ baseline_value: float | None = None
194
+ stable: bool = True
195
+ max_relative_deviation: float = 0.0
196
+ # GitHub #53: REQ-AIDS-056 requires classifying the conclusion as one of
197
+ # "stable", "attenuated", "reversed", or "not_comparable" rather than a
198
+ # bare relative-deviation threshold, which conflates a large same-signed
199
+ # change with a sign reversal and mishandles a near-zero baseline.
200
+ classification: str = "stable"
201
+ max_absolute_deviation: float = 0.0
202
+ sign_consistent: bool | None = None
203
+ magnitude_criterion: str = "relative_tolerance"
204
+
205
+ def __init__(
206
+ self,
207
+ *args: Any,
208
+ target_claim: Any = _UNSET,
209
+ results: Any = _UNSET,
210
+ baseline_value: Any = _UNSET,
211
+ stable: Any = _UNSET,
212
+ max_relative_deviation: Any = _UNSET,
213
+ classification: Any = _UNSET,
214
+ max_absolute_deviation: Any = _UNSET,
215
+ sign_consistent: Any = _UNSET,
216
+ magnitude_criterion: Any = _UNSET,
217
+ ) -> None:
218
+ fields = (
219
+ "results",
220
+ "baseline_value",
221
+ "stable",
222
+ "max_relative_deviation",
223
+ "classification",
224
+ "max_absolute_deviation",
225
+ "sign_consistent",
226
+ "magnitude_criterion",
227
+ )
228
+ values = {
229
+ "results": results,
230
+ "baseline_value": baseline_value,
231
+ "stable": stable,
232
+ "max_relative_deviation": max_relative_deviation,
233
+ "classification": classification,
234
+ "max_absolute_deviation": max_absolute_deviation,
235
+ "sign_consistent": sign_consistent,
236
+ "magnitude_criterion": magnitude_criterion,
237
+ }
238
+ if args and isinstance(args[0], str):
239
+ if len(args) > 9:
240
+ raise TypeError("SensitivityReport accepts at most 9 positional arguments.")
241
+ if target_claim is not _UNSET:
242
+ raise TypeError("SensitivityReport got multiple values for target_claim.")
243
+ for name, value in zip(fields, args[1:], strict=False):
244
+ if values[name] is not _UNSET:
245
+ raise TypeError(f"SensitivityReport got multiple values for {name}.")
246
+ target_claim = args[0]
247
+ for name, value in zip(fields, args[1:], strict=False):
248
+ values[name] = value
249
+ elif args:
250
+ if len(args) > 9:
251
+ raise TypeError("SensitivityReport accepts at most 9 positional arguments.")
252
+ for name, value in zip(fields, args[: len(fields)], strict=False):
253
+ if values[name] is not _UNSET:
254
+ raise TypeError(f"SensitivityReport got multiple values for {name}.")
255
+ for name, value in zip(fields, args[: len(fields)], strict=False):
256
+ values[name] = value
257
+ if len(args) > len(fields):
258
+ if target_claim is not _UNSET:
259
+ raise TypeError("SensitivityReport got multiple values for target_claim.")
260
+ target_claim = args[len(fields)]
261
+ if target_claim is _UNSET:
262
+ raise TypeError("SensitivityReport requires target_claim.")
263
+ defaults = {
264
+ "results": (),
265
+ "baseline_value": None,
266
+ "stable": True,
267
+ "max_relative_deviation": 0.0,
268
+ "classification": "stable",
269
+ "max_absolute_deviation": 0.0,
270
+ "sign_consistent": None,
271
+ "magnitude_criterion": "relative_tolerance",
272
+ }
273
+ object.__setattr__(self, "target_claim", target_claim)
274
+ for name, value in values.items():
275
+ if value is _UNSET:
276
+ value = defaults[name]
277
+ object.__setattr__(self, name, value)
278
+ _validate_target_claim(self.target_claim)
279
+
280
+
281
+ def _sign(value: float) -> int:
282
+ if value == 0:
283
+ return 0
284
+ return 1 if value > 0 else -1
285
+
286
+
287
+ # @id CODE-AIDS-127
288
+ # @implements REQ-AIDS-056
289
+ # @design DES-AIDS-044
290
+ def _classify_stability(
291
+ successful: list[SensitivityResult],
292
+ *,
293
+ baseline_value: float,
294
+ max_relative_deviation: float,
295
+ max_absolute_deviation: float,
296
+ stability_tolerance: float,
297
+ absolute_tolerance: float | None,
298
+ ) -> tuple[bool, str]:
299
+ """Return ``(sign_consistent, classification)`` for a non-empty ``successful``.
300
+
301
+ GitHub #53: classifies as ``"reversed"`` whenever successful values
302
+ disagree in sign (instead of a bare relative-deviation threshold, which
303
+ can mark a large same-signed drop "stable" and a small sign-consistent
304
+ change "unstable"), as ``"not_comparable"`` when the baseline is ``0``
305
+ and no ``absolute_tolerance`` was supplied (undefined relative
306
+ deviation), otherwise as ``"stable"``/``"attenuated"`` by whichever of
307
+ ``absolute_tolerance``/``stability_tolerance`` is configured.
308
+ """
309
+ signs = {_sign(r.value) for r in successful} - {0}
310
+ sign_consistent = len(signs) <= 1
311
+
312
+ if absolute_tolerance is not None:
313
+ within_tolerance = max_absolute_deviation <= absolute_tolerance
314
+ else:
315
+ within_tolerance = max_relative_deviation <= stability_tolerance
316
+
317
+ if not sign_consistent:
318
+ return sign_consistent, "reversed"
319
+ if baseline_value == 0 and absolute_tolerance is None:
320
+ return sign_consistent, "not_comparable"
321
+ if within_tolerance:
322
+ return sign_consistent, "stable"
323
+ return sign_consistent, "attenuated"
324
+
325
+
326
+ # @id CODE-AIDS-080
327
+ # @implements REQ-AIDS-056
328
+ # @design DES-AIDS-044
329
+ def run_sensitivity(
330
+ plan: SensitivityPlan,
331
+ analysis_fn: Callable[..., float],
332
+ stability_tolerance: float = 0.2,
333
+ absolute_tolerance: float | None = None,
334
+ ) -> SensitivityReport:
335
+ """Run ``analysis_fn`` across every specification in ``plan``.
336
+
337
+ ``analysis_fn`` is called once per specification as
338
+ ``analysis_fn(**specification)`` and must return a numeric
339
+ conclusion-relevant value; an exception it raises, or a non-finite
340
+ (``NaN``/``inf``) return value, is recorded on that specification's
341
+ ``SensitivityResult.failed``/``error`` instead of aborting the
342
+ remaining plan (REQ-AIDS-056). The first specification's value is
343
+ treated as the baseline.
344
+
345
+ The report's ``classification`` is one of:
346
+
347
+ - ``"reversed"``: at least one successful result's value has the
348
+ opposite sign from another (direction of the conclusion flips).
349
+ - ``"not_comparable"``: the baseline value is ``0`` and no
350
+ ``absolute_tolerance`` was given, so a relative deviation is
351
+ undefined (GitHub #53); or every specification failed.
352
+ - ``"stable"``: all successful values share a sign and the maximum
353
+ deviation from the baseline is within tolerance. Deviation is
354
+ measured by ``absolute_tolerance`` (against
355
+ ``max_absolute_deviation``) when given, otherwise by
356
+ ``stability_tolerance`` (against ``max_relative_deviation``); the
357
+ criterion actually used is recorded in ``magnitude_criterion``.
358
+ - ``"attenuated"``: all successful values share a sign but the
359
+ deviation exceeds the configured tolerance.
360
+
361
+ ``stable`` (bool) is kept for backward compatibility and is ``True``
362
+ exactly when ``classification == "stable"``.
363
+ """
364
+ specifications = plan.specifications()
365
+ results = []
366
+ for spec in specifications:
367
+ try:
368
+ value = float(analysis_fn(**spec))
369
+ if not math.isfinite(value):
370
+ # GitHub #53 follow-up (rubber-duck review): a non-finite
371
+ # value (NaN/inf) is not a meaningfully comparable
372
+ # conclusion value; treat it as a failed specification
373
+ # rather than letting it masquerade as "stable".
374
+ raise ValueError(f"analysis_fn returned a non-finite value: {value!r}")
375
+ except Exception as exc: # noqa: BLE001 - recorded, not re-raised
376
+ results.append(
377
+ SensitivityResult(
378
+ target_claim=plan.target_claim,
379
+ specification=spec,
380
+ value=float("nan"),
381
+ failed=True,
382
+ error=str(exc),
383
+ )
384
+ )
385
+ else:
386
+ results.append(
387
+ SensitivityResult(target_claim=plan.target_claim, specification=spec, value=value)
388
+ )
389
+ results = tuple(results)
390
+
391
+ if not results:
392
+ return SensitivityReport(
393
+ target_claim=plan.target_claim,
394
+ results=(),
395
+ baseline_value=None,
396
+ stable=False,
397
+ classification="not_comparable",
398
+ )
399
+
400
+ successful = [r for r in results if not r.failed]
401
+ if not successful:
402
+ return SensitivityReport(
403
+ target_claim=plan.target_claim,
404
+ results=results,
405
+ baseline_value=None,
406
+ stable=False,
407
+ classification="not_comparable",
408
+ )
409
+
410
+ baseline_value = successful[0].value
411
+ magnitude_criterion = (
412
+ "absolute_tolerance" if absolute_tolerance is not None else "relative_tolerance"
413
+ )
414
+
415
+ max_relative_deviation = 0.0
416
+ max_absolute_deviation = 0.0
417
+ for result in successful[1:]:
418
+ absolute_deviation = abs(result.value - baseline_value)
419
+ if baseline_value == 0:
420
+ relative_deviation = absolute_deviation
421
+ else:
422
+ relative_deviation = absolute_deviation / abs(baseline_value)
423
+ max_relative_deviation = max(max_relative_deviation, relative_deviation)
424
+ max_absolute_deviation = max(max_absolute_deviation, absolute_deviation)
425
+
426
+ sign_consistent, classification = _classify_stability(
427
+ successful,
428
+ baseline_value=baseline_value,
429
+ max_relative_deviation=max_relative_deviation,
430
+ max_absolute_deviation=max_absolute_deviation,
431
+ stability_tolerance=stability_tolerance,
432
+ absolute_tolerance=absolute_tolerance,
433
+ )
434
+
435
+ return SensitivityReport(
436
+ target_claim=plan.target_claim,
437
+ results=results,
438
+ baseline_value=baseline_value,
439
+ stable=classification == "stable",
440
+ max_relative_deviation=max_relative_deviation,
441
+ classification=classification,
442
+ max_absolute_deviation=max_absolute_deviation,
443
+ sign_consistent=sign_consistent,
444
+ magnitude_criterion=magnitude_criterion,
445
+ )
@@ -0,0 +1,201 @@
1
+ """Spectral signal analysis: baseline correction and peak/FWHM detection.
2
+
3
+ Implements DES-AIDS-094 (REQ-AIDS-094, REQ-AIDS-095, REQ-AIDS-096): a
4
+ first-class module for spectral/scientific-signal analysis (GitHub #74)
5
+ operating on a generic 2-column ``x, y`` spectrum — baseline correction
6
+ (linear two-point fit or Asymmetric Least Squares), a
7
+ ``scipy.signal.find_peaks``/``peak_widths`` wrapper returning
8
+ ``{position, fwhm, prominence, height}`` dicts, and a thin helper that
9
+ builds a ``sensitivity.SensitivityPlan`` over
10
+ ``(prominence_frac, window)`` so a peak-count stability check requires
11
+ no caller-written glue code around ``sensitivity.run_sensitivity``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import numbers
17
+ from collections.abc import Callable, Sequence
18
+ from typing import Any
19
+
20
+ import numpy as np
21
+ import scipy.sparse
22
+ import scipy.sparse.linalg
23
+ from scipy.signal import find_peaks, peak_widths, savgol_filter
24
+
25
+ from ai_data_scientist.sensitivity import SensitivityPlan
26
+
27
+ _SUPPORTED_BASELINE_METHODS = ("linear", "asls")
28
+
29
+ # Fixed internal AsLS (Eilers & Boelens, 2005) parameters (DES-AIDS-094 /
30
+ # ADR-0112): not part of the public signature, so behavior is fully
31
+ # deterministic for a given x/y/method.
32
+ _ASLS_LAM = 1e5
33
+ _ASLS_P = 0.001
34
+ _ASLS_N_ITER = 10
35
+
36
+
37
+ # @id CODE-AIDS-146
38
+ # @implements REQ-AIDS-094 REQ-AIDS-095
39
+ # @design DES-AIDS-094
40
+ def _coerce_xy(
41
+ x: Sequence[float], y: Sequence[float], *, min_len: int = 2
42
+ ) -> tuple[np.ndarray, np.ndarray]:
43
+ x_arr = np.array(x, dtype=float, copy=True)
44
+ y_arr = np.array(y, dtype=float, copy=True)
45
+ if x_arr.ndim != 1 or y_arr.ndim != 1:
46
+ raise ValueError("x and y must be 1-D sequences.")
47
+ if x_arr.shape[0] != y_arr.shape[0]:
48
+ raise ValueError("x and y must have equal length.")
49
+ if x_arr.shape[0] < min_len:
50
+ raise ValueError(f"x/y must have at least {min_len} elements.")
51
+ if not (np.all(np.isfinite(x_arr)) and np.all(np.isfinite(y_arr))):
52
+ raise ValueError("x and y must contain only finite values.")
53
+ return x_arr, y_arr
54
+
55
+
56
+ # @id CODE-AIDS-141
57
+ # @implements REQ-AIDS-094
58
+ # @design DES-AIDS-094
59
+ def _linear_baseline(x_arr: np.ndarray, y_arr: np.ndarray) -> np.ndarray:
60
+ if x_arr[0] == x_arr[-1]:
61
+ raise ValueError("x[0] and x[-1] must differ for a linear baseline fit.")
62
+ slope = (y_arr[-1] - y_arr[0]) / (x_arr[-1] - x_arr[0])
63
+ return y_arr[0] + slope * (x_arr - x_arr[0])
64
+
65
+
66
+ # @id CODE-AIDS-142
67
+ # @implements REQ-AIDS-094
68
+ # @design DES-AIDS-094
69
+ def _asls_baseline(y_arr: np.ndarray) -> np.ndarray:
70
+ n = y_arr.shape[0]
71
+ if n < 3:
72
+ raise ValueError("AsLS baseline correction requires at least 3 elements.")
73
+ diff_matrix = scipy.sparse.diags([1.0, -2.0, 1.0], offsets=[0, 1, 2], shape=(n - 2, n))
74
+ penalty = _ASLS_LAM * (diff_matrix.T @ diff_matrix)
75
+ weights = np.ones(n, dtype=float)
76
+ baseline = y_arr
77
+ for iteration in range(_ASLS_N_ITER):
78
+ weight_matrix = scipy.sparse.diags(weights, 0, shape=(n, n))
79
+ baseline = scipy.sparse.linalg.spsolve((weight_matrix + penalty).tocsc(), weights * y_arr)
80
+ if iteration < _ASLS_N_ITER - 1:
81
+ weights = np.where(y_arr > baseline, _ASLS_P, 1.0 - _ASLS_P)
82
+ return baseline
83
+
84
+
85
+ # @id CODE-AIDS-143
86
+ # @implements REQ-AIDS-094
87
+ # @design DES-AIDS-094
88
+ def baseline_correct(x: Sequence[float], y: Sequence[float], method: str = "linear") -> np.ndarray:
89
+ """Return ``y`` with an estimated baseline subtracted (GitHub #74).
90
+
91
+ ``method="linear"`` fits a straight line through the first and last
92
+ points; ``method="asls"`` uses Asymmetric Least Squares smoothing
93
+ with fixed internal parameters (``lam=1e5``, ``p=0.001``,
94
+ ``n_iter=10``). Neither ``x`` nor ``y`` is mutated.
95
+ """
96
+ if method not in _SUPPORTED_BASELINE_METHODS:
97
+ raise ValueError(f"Unsupported baseline_correct method: {method!r}")
98
+ min_len = 3 if method == "asls" else 2
99
+ x_arr, y_arr = _coerce_xy(x, y, min_len=min_len)
100
+ if method == "linear":
101
+ baseline = _linear_baseline(x_arr, y_arr)
102
+ else:
103
+ baseline = _asls_baseline(y_arr)
104
+ return y_arr - baseline
105
+
106
+
107
+ # @id CODE-AIDS-147
108
+ # @implements REQ-AIDS-095
109
+ # @design DES-AIDS-094
110
+ def _validate_window(window: Any, n: int) -> int:
111
+ if not isinstance(window, numbers.Integral) or isinstance(window, bool):
112
+ raise ValueError( # noqa: TRY004 - ValueError required by REQ-AIDS-095
113
+ "window must be an odd integer (bool is not accepted)."
114
+ )
115
+ window = int(window)
116
+ if window % 2 == 0 or window < 5 or window > n:
117
+ raise ValueError(f"window must be an odd integer in [5, {n}]; got {window}.")
118
+ return window
119
+
120
+
121
+ # @id CODE-AIDS-144
122
+ # @implements REQ-AIDS-095
123
+ # @design DES-AIDS-094
124
+ def find_spectral_peaks(
125
+ x: Sequence[float],
126
+ y: Sequence[float],
127
+ prominence_frac: float = 0.05,
128
+ window: int | None = None,
129
+ ) -> list[dict[str, float]]:
130
+ """Return ``{position, fwhm, prominence, height}`` dicts for each peak.
131
+
132
+ ``x`` must be strictly increasing and uniformly spaced. When
133
+ ``window`` is given, ``y`` is Savitzky-Golay smoothed (``polyorder``
134
+ fixed at 3) before detection, and every returned value is computed
135
+ from that smoothed ``y`` (GitHub #74).
136
+ """
137
+ x_arr, y_arr = _coerce_xy(x, y, min_len=2)
138
+ diffs = np.diff(x_arr)
139
+ if not np.all(diffs > 0):
140
+ raise ValueError("x must be strictly increasing.")
141
+ dx = x_arr[1] - x_arr[0]
142
+ if not np.allclose(diffs, dx, rtol=1e-6, atol=0.0):
143
+ raise ValueError("x must be uniformly spaced.")
144
+
145
+ n = x_arr.shape[0]
146
+ if window is not None:
147
+ window = _validate_window(window, n)
148
+ y_arr = savgol_filter(y_arr, window_length=window, polyorder=3)
149
+
150
+ y_range = y_arr.max() - y_arr.min()
151
+ indices, properties = find_peaks(y_arr, prominence=prominence_frac * y_range)
152
+ if indices.size == 0:
153
+ return []
154
+
155
+ widths_samples, *_rest = peak_widths(y_arr, indices, rel_height=0.5)
156
+
157
+ peaks = []
158
+ for position_index, width_samples, prominence in zip(
159
+ indices, widths_samples, properties["prominences"], strict=True
160
+ ):
161
+ peaks.append(
162
+ {
163
+ "position": float(x_arr[position_index]),
164
+ "fwhm": float(width_samples * dx),
165
+ "prominence": float(prominence),
166
+ "height": float(y_arr[position_index]),
167
+ }
168
+ )
169
+ return peaks
170
+
171
+
172
+ # @id CODE-AIDS-145
173
+ # @implements REQ-AIDS-096
174
+ # @design DES-AIDS-094
175
+ def build_peak_sensitivity_plan(
176
+ x: Sequence[float],
177
+ y: Sequence[float],
178
+ prominence_fracs: list[float],
179
+ windows: list[int | None],
180
+ target_claim: str,
181
+ max_runs: int = 100,
182
+ ) -> tuple[SensitivityPlan, Callable[..., float]]:
183
+ """Return a ``(SensitivityPlan, analysis_fn)`` pair for peak-count stability.
184
+
185
+ ``analysis_fn`` calls :func:`find_spectral_peaks` with the captured
186
+ ``x``/``y`` and the specification's ``prominence_frac``/``window``,
187
+ returning the peak count as a ``float`` — so the pair can be passed
188
+ unchanged to ``sensitivity.run_sensitivity`` with no caller-written
189
+ glue code (GitHub #74).
190
+ """
191
+ plan = SensitivityPlan(
192
+ target_claim=target_claim,
193
+ parameter_grid={"prominence_frac": prominence_fracs, "window": windows},
194
+ max_runs=max_runs,
195
+ )
196
+
197
+ def analysis_fn(*, prominence_frac: float, window: int | None) -> float:
198
+ peaks = find_spectral_peaks(x, y, prominence_frac=prominence_frac, window=window)
199
+ return float(len(peaks))
200
+
201
+ return plan, analysis_fn
@@ -0,0 +1,40 @@
1
+ """Skill packaging metadata.
2
+
3
+ Implements DES-AIDS-001 (REQ-AIDS-012): parses the GitHub Copilot Agent
4
+ Skill manifest (YAML frontmatter + Markdown sections) so packaging can be
5
+ verified the same way other `sdd-*` skills in this repository are
6
+ discovered and loaded.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ _REPO_ROOT = Path(__file__).resolve().parents[2]
15
+ DEFAULT_SKILL_PATH = _REPO_ROOT / ".github" / "skills" / "ai-data-scientist" / "SKILL.md"
16
+
17
+ _FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n(.*)$", re.DOTALL)
18
+ _FIELD_RE = re.compile(r'^(\w+):\s*"?(.*?)"?\s*$', re.MULTILINE)
19
+ _SECTION_RE = re.compile(r"^#+\s+(.+)$", re.MULTILINE)
20
+
21
+
22
+ # @id CODE-AIDS-012
23
+ # @implements REQ-AIDS-012
24
+ # @design DES-AIDS-001
25
+ def load_skill_manifest(path: Path = DEFAULT_SKILL_PATH) -> dict:
26
+ """Parse a SKILL.md file's frontmatter and section headings."""
27
+ text = Path(path).read_text(encoding="utf-8")
28
+ match = _FRONTMATTER_RE.match(text)
29
+ if not match:
30
+ raise ValueError(f"'{path}' is missing the required YAML frontmatter block.")
31
+
32
+ frontmatter_block, body = match.groups()
33
+ fields = dict(_FIELD_RE.findall(frontmatter_block))
34
+ sections = _SECTION_RE.findall(body)
35
+
36
+ return {
37
+ "name": fields.get("name", ""),
38
+ "description": fields.get("description", ""),
39
+ "sections": sections,
40
+ }