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.
- package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
- package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
- package/.github/skills/ai-data-scientist/SKILL.md +330 -0
- package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
- package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
- package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
- package/.github/skills/ai-materials-scientist/manifest.json +58 -0
- package/.github/skills/ai-scientist/SKILL.md +69 -0
- package/.github/skills/ai-scientist/manifest.json +61 -0
- package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
- package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
- package/.github/skills/japanese-prose/NOTICE.md +17 -0
- package/.github/skills/japanese-prose/SKILL.md +111 -0
- package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
- package/.github/skills/japanese-prose/references/scoring.md +24 -0
- package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
- package/.github/skills/japanese-prose/scripts/core.py +192 -0
- package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
- package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
- package/.github/skills/japanese-prose/scripts/lint.py +378 -0
- package/.github/skills/japanese-prose/scripts/outline.py +68 -0
- package/.github/skills/japanese-prose/scripts/terms.py +112 -0
- package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
- package/.github/skills/presentation-planner/SKILL.md +257 -0
- package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
- package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
- package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
- package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
- package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
- package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
- package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
- package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
- package/.github/skills/tech-writer/SKILL.md +434 -0
- package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
- package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
- package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
- package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
- package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
- package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
- package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
- package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
- package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
- package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
- package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
- package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
- package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
- package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
- package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
- package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
- package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
- package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
- package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
- package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
- package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
- package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
- package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
- package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
- package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
- package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
- package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
- package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
- package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
- package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
- package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
- package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
- package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
- package/.github/skills/tech-writer/references/style-constitution.md +104 -0
- package/.github/skills/tech-writer/scripts/lint.py +412 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/ai-data-scientist.js +123 -0
- package/package.json +41 -0
- package/pyproject.toml +45 -0
- package/src/ai_chemistry_scientist/__init__.py +0 -0
- package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
- package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
- package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
- package/src/ai_chemistry_scientist/dispatch.py +369 -0
- package/src/ai_chemistry_scientist/docking_score.py +97 -0
- package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
- package/src/ai_chemistry_scientist/evidence.py +41 -0
- package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
- package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
- package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
- package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
- package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
- package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
- package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
- package/src/ai_chemistry_scientist/validation.py +70 -0
- package/src/ai_data_scientist/__init__.py +0 -0
- package/src/ai_data_scientist/analysis_assumptions.py +121 -0
- package/src/ai_data_scientist/anomaly_detection.py +39 -0
- package/src/ai_data_scientist/automl.py +109 -0
- package/src/ai_data_scientist/cleaning.py +56 -0
- package/src/ai_data_scientist/cli.py +90 -0
- package/src/ai_data_scientist/clustering.py +54 -0
- package/src/ai_data_scientist/dashboard.py +33 -0
- package/src/ai_data_scientist/data_definition.py +100 -0
- package/src/ai_data_scientist/data_quality.py +164 -0
- package/src/ai_data_scientist/dataset_validation.py +135 -0
- package/src/ai_data_scientist/dependency_pins.py +60 -0
- package/src/ai_data_scientist/eda.py +82 -0
- package/src/ai_data_scientist/experiment_evaluation.py +635 -0
- package/src/ai_data_scientist/explainability.py +340 -0
- package/src/ai_data_scientist/feature_engineering.py +163 -0
- package/src/ai_data_scientist/gate_config.py +32 -0
- package/src/ai_data_scientist/ingestion.py +127 -0
- package/src/ai_data_scientist/insight_engine.py +180 -0
- package/src/ai_data_scientist/japanese_nlp.py +43 -0
- package/src/ai_data_scientist/jupyter_launcher.py +137 -0
- package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
- package/src/ai_data_scientist/language_router.py +28 -0
- package/src/ai_data_scientist/lifecycle.py +221 -0
- package/src/ai_data_scientist/mcp_gateway.py +113 -0
- package/src/ai_data_scientist/mcp_runtime.py +194 -0
- package/src/ai_data_scientist/mcp_transport.py +53 -0
- package/src/ai_data_scientist/ml_modeling.py +451 -0
- package/src/ai_data_scientist/model_tuning.py +104 -0
- package/src/ai_data_scientist/notebook_audit.py +574 -0
- package/src/ai_data_scientist/project_manager.py +243 -0
- package/src/ai_data_scientist/report_export.py +73 -0
- package/src/ai_data_scientist/sensitivity.py +445 -0
- package/src/ai_data_scientist/signal_analysis.py +201 -0
- package/src/ai_data_scientist/skill_packaging.py +40 -0
- package/src/ai_data_scientist/stats_analysis.py +88 -0
- package/src/ai_data_scientist/text_nlp.py +44 -0
- package/src/ai_data_scientist/timeseries.py +68 -0
- package/src/ai_data_scientist/visualization.py +708 -0
- package/src/ai_genomics_scientist/__init__.py +1 -0
- package/src/ai_genomics_scientist/differential_expression.py +147 -0
- package/src/ai_genomics_scientist/dispatch.py +267 -0
- package/src/ai_genomics_scientist/evidence.py +45 -0
- package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
- package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
- package/src/ai_genomics_scientist/sequence_features.py +111 -0
- package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
- package/src/ai_genomics_scientist/validation.py +83 -0
- package/src/ai_genomics_scientist/variant_effect.py +147 -0
- package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
- package/src/ai_materials_scientist/__init__.py +0 -0
- package/src/ai_materials_scientist/calphad.py +117 -0
- package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
- package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
- package/src/ai_materials_scientist/dispatch.py +100 -0
- package/src/ai_materials_scientist/evidence.py +84 -0
- package/src/ai_materials_scientist/fem.py +279 -0
- package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
- package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
- package/src/ai_materials_scientist/phase_field.py +167 -0
- package/src/ai_materials_scientist/validation.py +70 -0
- package/src/ai_scientist/__init__.py +1 -0
- package/src/ai_scientist/completion_gate.py +15 -0
- package/src/ai_scientist/data_analysis.py +46 -0
- package/src/ai_scientist/evidence_registry.py +99 -0
- package/src/ai_scientist/experimental_design.py +20 -0
- package/src/ai_scientist/language.py +14 -0
- package/src/ai_scientist/latex_renderer.py +41 -0
- package/src/ai_scientist/literature_review.py +37 -0
- package/src/ai_scientist/manifest.py +87 -0
- package/src/ai_scientist/manuscript.py +94 -0
- package/src/ai_scientist/mcp_config.py +76 -0
- package/src/ai_scientist/mcp_external.py +42 -0
- package/src/ai_scientist/mcp_failures.py +23 -0
- package/src/ai_scientist/mcp_gateway.py +38 -0
- package/src/ai_scientist/mcp_managed.py +180 -0
- package/src/ai_scientist/npm_packaging.py +49 -0
- package/src/ai_scientist/orchestrator.py +133 -0
- package/src/ai_scientist/peer_review.py +60 -0
- package/src/ai_scientist/phase_gate.py +74 -0
- package/src/ai_scientist/phase_state.py +230 -0
- package/src/ai_scientist/presentation.py +56 -0
- package/src/ai_scientist/project_config.py +31 -0
- package/src/ai_scientist/project_handle.py +74 -0
- package/src/ai_scientist/reproducibility.py +20 -0
- package/src/ai_scientist/research_planning.py +20 -0
- package/src/ai_scientist/skill_invocation.py +21 -0
- package/src/ai_scientist/tdd_gate.py +99 -0
- package/src/ai_structural_biology_scientist/__init__.py +0 -0
- package/src/ai_structural_biology_scientist/contact_map.py +87 -0
- package/src/ai_structural_biology_scientist/dispatch.py +269 -0
- package/src/ai_structural_biology_scientist/evidence.py +43 -0
- package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
- package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
- package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
- package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
- 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
|
+
}
|