scientific-computing-system 1.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. cds/__init__.py +102 -0
  2. cds/__main__.py +8 -0
  3. cds/_version.py +10 -0
  4. cds/cli/__init__.py +104 -0
  5. cds/cli/__main__.py +8 -0
  6. cds/cli/_handlers.py +405 -0
  7. cds/cli/_parser.py +156 -0
  8. cds/cli/_style.py +117 -0
  9. cds/core/__init__.py +5 -0
  10. cds/core/_numeric.py +92 -0
  11. cds/core/models.py +166 -0
  12. cds/data_analysis/__init__.py +30 -0
  13. cds/data_analysis/dataset.py +122 -0
  14. cds/data_analysis/loader.py +75 -0
  15. cds/data_analysis/pandas_io.py +108 -0
  16. cds/data_analysis/transform.py +36 -0
  17. cds/data_analysis/viz.py +88 -0
  18. cds/diffeq/__init__.py +29 -0
  19. cds/diffeq/_implicit.py +349 -0
  20. cds/diffeq/solvers.py +312 -0
  21. cds/graph/__init__.py +31 -0
  22. cds/graph/algorithms.py +399 -0
  23. cds/hypothesis/__init__.py +48 -0
  24. cds/hypothesis/evaluator.py +369 -0
  25. cds/hypothesis/generator.py +275 -0
  26. cds/knowledge/__init__.py +41 -0
  27. cds/knowledge/graph.py +489 -0
  28. cds/knowledge/notes.py +231 -0
  29. cds/knowledge/retrieval.py +167 -0
  30. cds/math_utils/__init__.py +41 -0
  31. cds/math_utils/calculus.py +40 -0
  32. cds/math_utils/linalg.py +461 -0
  33. cds/math_utils/special.py +167 -0
  34. cds/ml/__init__.py +25 -0
  35. cds/ml/clustering.py +190 -0
  36. cds/ml/decomposition.py +172 -0
  37. cds/ml/linear_models.py +225 -0
  38. cds/ml/neighbors.py +162 -0
  39. cds/ml/neural.py +204 -0
  40. cds/ml/preprocessing.py +131 -0
  41. cds/ml/tree.py +202 -0
  42. cds/modeling/__init__.py +40 -0
  43. cds/modeling/_base.py +210 -0
  44. cds/modeling/_nodes.py +526 -0
  45. cds/modeling/expression.py +81 -0
  46. cds/modeling/model.py +134 -0
  47. cds/modeling/solver.py +176 -0
  48. cds/montecarlo/__init__.py +21 -0
  49. cds/montecarlo/methods.py +272 -0
  50. cds/nlp/__init__.py +170 -0
  51. cds/nlp/attention.py +285 -0
  52. cds/nlp/autograd/__init__.py +70 -0
  53. cds/nlp/autograd/_grad.py +120 -0
  54. cds/nlp/autograd/ops.py +133 -0
  55. cds/nlp/autograd/tensor.py +298 -0
  56. cds/nlp/bpe.py +449 -0
  57. cds/nlp/data.py +70 -0
  58. cds/nlp/embed.py +190 -0
  59. cds/nlp/layers.py +259 -0
  60. cds/nlp/model.py +456 -0
  61. cds/nlp/optim.py +163 -0
  62. cds/nlp/training.py +167 -0
  63. cds/nlp/viz.py +254 -0
  64. cds/numerical_integration/__init__.py +31 -0
  65. cds/numerical_integration/quadrature.py +480 -0
  66. cds/optimization/__init__.py +18 -0
  67. cds/optimization/_metaheuristics.py +287 -0
  68. cds/optimization/minimize.py +425 -0
  69. cds/plot/__init__.py +49 -0
  70. cds/plot/_backend.py +32 -0
  71. cds/plot/charts.py +462 -0
  72. cds/probability/__init__.py +55 -0
  73. cds/probability/_advanced.py +283 -0
  74. cds/probability/distributions.py +236 -0
  75. cds/py.typed +0 -0
  76. cds/quantum/__init__.py +51 -0
  77. cds/quantum/circuit.py +92 -0
  78. cds/quantum/multi_qubit.py +255 -0
  79. cds/quantum/simulator.py +38 -0
  80. cds/scientific/__init__.py +34 -0
  81. cds/scientific/constants.py +35 -0
  82. cds/scientific/formulas.py +116 -0
  83. cds/signals/__init__.py +43 -0
  84. cds/signals/filters.py +421 -0
  85. cds/signals/processing.py +224 -0
  86. cds/stats/__init__.py +83 -0
  87. cds/stats/_distributions.py +102 -0
  88. cds/stats/descriptive.py +172 -0
  89. cds/stats/hypothesis_tests.py +403 -0
  90. cds/stats/nonparametric.py +131 -0
  91. cds/stats/regression.py +51 -0
  92. cds/stats/time_series.py +408 -0
  93. scientific_computing_system-1.6.2.dist-info/METADATA +741 -0
  94. scientific_computing_system-1.6.2.dist-info/RECORD +97 -0
  95. scientific_computing_system-1.6.2.dist-info/WHEEL +4 -0
  96. scientific_computing_system-1.6.2.dist-info/entry_points.txt +2 -0
  97. scientific_computing_system-1.6.2.dist-info/licenses/LICENSE +21 -0
cds/cli/_parser.py ADDED
@@ -0,0 +1,156 @@
1
+ """Argument-parser construction for the ``cds`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from cds.cli._handlers import (
8
+ _cmd_benchmark,
9
+ _cmd_calc,
10
+ _cmd_constants,
11
+ _cmd_dashboard,
12
+ _cmd_hypothesis,
13
+ _cmd_info,
14
+ _cmd_integrate,
15
+ _cmd_modules,
16
+ _cmd_plot,
17
+ _cmd_prompt,
18
+ _cmd_sample,
19
+ _cmd_stats,
20
+ _cmd_version,
21
+ )
22
+
23
+ _DOMAIN_CHOICES = ["physics", "cosmology", "mathematics", "biology", "chemistry", "general_science"]
24
+
25
+
26
+ def build_parser() -> argparse.ArgumentParser:
27
+ """Construct the top-level ``cds`` argument parser and its subcommands."""
28
+ parser = argparse.ArgumentParser(
29
+ prog="cds",
30
+ description="Cognitive Discovery System — computational science platform.",
31
+ )
32
+ parser.add_argument("--version", "-v", action="store_true", help="Show System version and exit")
33
+
34
+ sub = parser.add_subparsers(dest="command")
35
+
36
+ p_version = sub.add_parser("version", help="Show System version.")
37
+ p_version.set_defaults(func=_cmd_version)
38
+
39
+ p_hyp = sub.add_parser(
40
+ "hypothesis", help="Generate scientific hypotheses for a research question."
41
+ )
42
+ p_hyp.add_argument("question", help="The core research question or problem")
43
+ p_hyp.add_argument(
44
+ "--domain",
45
+ "-d",
46
+ default="general_science",
47
+ choices=_DOMAIN_CHOICES,
48
+ help="Scientific domain focus",
49
+ )
50
+ p_hyp.add_argument("--num", "-n", type=int, default=3, help="Number of hypotheses to propose")
51
+ p_hyp.add_argument("--output", "-o", help="Save results as JSON")
52
+ p_hyp.add_argument("--show-prompt", action="store_true", help="Print the exact prompt template")
53
+ p_hyp.add_argument("--dry-run", action="store_true", help="Do not run generation logic")
54
+ p_hyp.set_defaults(func=_cmd_hypothesis)
55
+
56
+ p_prompt = sub.add_parser("prompt", help="Print a ready-to-use prompt for a custom generator.")
57
+ p_prompt.add_argument("question", help="Research question")
58
+ p_prompt.add_argument(
59
+ "--domain",
60
+ "-d",
61
+ default="general_science",
62
+ choices=_DOMAIN_CHOICES,
63
+ help="Scientific domain focus",
64
+ )
65
+ p_prompt.add_argument(
66
+ "--num", "-n", type=int, default=3, help="Number of hypotheses to propose"
67
+ )
68
+ p_prompt.set_defaults(func=_cmd_prompt)
69
+
70
+ p_info = sub.add_parser("info", help="Show System info, module status, and System health.")
71
+ p_info.set_defaults(func=_cmd_info)
72
+
73
+ sub.add_parser("dashboard", help="Launch the interactive System dashboard.").set_defaults(
74
+ func=_cmd_dashboard
75
+ )
76
+ sub.add_parser("benchmark", help="Run built-in benchmarks to verify performance.").set_defaults(
77
+ func=_cmd_benchmark
78
+ )
79
+ sub.add_parser("constants", help="List available physical constants.").set_defaults(
80
+ func=_cmd_constants
81
+ )
82
+
83
+ p_plot = sub.add_parser(
84
+ "plot",
85
+ help="Plot a series of numbers (ASCII terminal, or PNG with --file if cds[plot] installed).",
86
+ )
87
+ p_plot.add_argument("values", help="Comma-separated list of numbers (e.g. '1,5,3,8')")
88
+ p_plot.add_argument("--title", "-t", default="CLI Plot", help="Title of the plot")
89
+ p_plot.add_argument(
90
+ "--file",
91
+ "-f",
92
+ default=None,
93
+ help="Save a PNG via optional matplotlib (requires cds[plot]); omit for ASCII",
94
+ )
95
+ p_plot.add_argument(
96
+ "--kind",
97
+ "-k",
98
+ default="series",
99
+ choices=["series", "hist", "acf"],
100
+ help="Chart type when using --file (default: series)",
101
+ )
102
+ p_plot.set_defaults(func=_cmd_plot)
103
+
104
+ p_calc = sub.add_parser("calc", help="Quick physics calculations.")
105
+ p_calc.add_argument("formula", help="Formula: ke, gravity, wave, gas")
106
+ p_calc.set_defaults(func=_cmd_calc)
107
+
108
+ p_stats = sub.add_parser(
109
+ "stats", help="Descriptive statistics for a comma-separated number list."
110
+ )
111
+ p_stats.add_argument("values", help="Comma-separated numbers (e.g. '1,2,3,4')")
112
+ p_stats.set_defaults(func=_cmd_stats)
113
+
114
+ p_sample = sub.add_parser("sample", help="Draw samples from a probability distribution.")
115
+ p_sample.add_argument(
116
+ "dist",
117
+ choices=["uniform", "gaussian", "exponential", "poisson"],
118
+ help="Distribution name",
119
+ )
120
+ p_sample.add_argument("-n", type=int, default=5, help="Number of samples (default 5)")
121
+ p_sample.add_argument("--seed", type=int, default=None, help="RNG seed")
122
+ p_sample.add_argument("--a", type=float, default=0.0, help="uniform lower bound")
123
+ p_sample.add_argument("--b", type=float, default=1.0, help="uniform upper bound")
124
+ p_sample.add_argument("--mu", type=float, default=0.0, help="gaussian mean")
125
+ p_sample.add_argument("--sigma", type=float, default=1.0, help="gaussian stdev")
126
+ p_sample.add_argument("--lam", type=float, default=1.0, help="rate λ for exponential/poisson")
127
+ p_sample.set_defaults(func=_cmd_sample)
128
+
129
+ p_int = sub.add_parser(
130
+ "integrate", help="Numerically integrate a built-in function over [a, b]."
131
+ )
132
+ p_int.add_argument(
133
+ "integrand",
134
+ choices=["sin", "cos", "exp", "x2", "unit"],
135
+ help="Integrand name",
136
+ )
137
+ p_int.add_argument("--a", type=float, default=0.0, help="Lower limit (default 0)")
138
+ p_int.add_argument("--b", type=float, default=1.0, help="Upper limit (default 1)")
139
+ p_int.add_argument("-n", type=int, default=1000, help="Number of panels (default 1000)")
140
+ p_int.add_argument(
141
+ "--method",
142
+ choices=["simpson", "trap"],
143
+ default="simpson",
144
+ help="Quadrature rule (default simpson)",
145
+ )
146
+ p_int.set_defaults(func=_cmd_integrate)
147
+
148
+ sub.add_parser(
149
+ "modules", help="List all scientific modules available in the System."
150
+ ).set_defaults(func=_cmd_modules)
151
+
152
+ return parser
153
+
154
+
155
+ # Historical private name kept as an alias for backwards compatibility.
156
+ _build_parser = build_parser
cds/cli/_style.py ADDED
@@ -0,0 +1,117 @@
1
+ """ANSI colour helpers and ASCII table rendering for the CDS CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ _RESET = "\033[0m"
8
+ _STYLES: dict[str, str] = {
9
+ "bold": "\033[1m",
10
+ "dim": "\033[2m",
11
+ "italic": "\033[3m",
12
+ "red": "\033[31m",
13
+ "green": "\033[32m",
14
+ "yellow": "\033[33m",
15
+ "blue": "\033[34m",
16
+ "magenta": "\033[35m",
17
+ "cyan": "\033[36m",
18
+ "bold green": "\033[1;32m",
19
+ "bold blue": "\033[1;34m",
20
+ "bold cyan": "\033[1;36m",
21
+ "bold magenta": "\033[1;35m",
22
+ "bold red": "\033[1;31m",
23
+ "bold yellow": "\033[1;33m",
24
+ }
25
+
26
+
27
+ def _supports_color() -> bool:
28
+ """Return True when stdout looks like an interactive colour terminal."""
29
+ return sys.stdout.isatty()
30
+
31
+
32
+ def _wrap(style: str, text: str) -> str:
33
+ """Wrap ``text`` in the ANSI codes for ``style`` if stdout is a TTY."""
34
+ if not _supports_color():
35
+ return text
36
+ code = _STYLES.get(style, "")
37
+ if not code:
38
+ return text
39
+ return f"{code}{text}{_RESET}"
40
+
41
+
42
+ def _print(*args: object) -> None:
43
+ """Print helper routed through stdout (so tests can capture via capsys)."""
44
+ print(*args)
45
+
46
+
47
+ def _render(markup: str) -> str:
48
+ """Render rich-style ``[style]...[/]`` markup into ANSI-coloured text.
49
+
50
+ Only the tag shapes actually used by this CLI are supported:
51
+ ``[bold]``, ``[italic]``, ``[dim]``, ``[red]``, ``[green]``, ``[yellow]``,
52
+ ``[blue]``, ``[cyan]``, ``[magenta]`` and a few combined ``[bold green]``
53
+ variants. Unknown tags are stripped to their inner text. Nesting is not
54
+ supported (the CLI never nests them).
55
+ """
56
+ out: list[str] = []
57
+ i = 0
58
+ n = len(markup)
59
+ while i < n:
60
+ open_idx = markup.find("[", i)
61
+ if open_idx == -1:
62
+ out.append(markup[i:])
63
+ break
64
+ out.append(markup[i:open_idx])
65
+ close_idx = markup.find("]", open_idx)
66
+ if close_idx == -1:
67
+ out.append(markup[open_idx:])
68
+ break
69
+ tag = markup[open_idx + 1 : close_idx]
70
+ # Closing tag [/] ends the current styled run.
71
+ if tag.startswith("/"):
72
+ out.append(_RESET if _supports_color() else "")
73
+ i = close_idx + 1
74
+ continue
75
+ # Combined forms like "bold green" are looked up directly.
76
+ code = _STYLES.get(tag)
77
+ if code is None and " " in tag:
78
+ # ``[bold green]`` style — already in the table; fall back to first word.
79
+ code = _STYLES.get(tag.split()[0], "")
80
+ out.append(code if (_supports_color() and code is not None) else "")
81
+ i = close_idx + 1
82
+ return "".join(out)
83
+
84
+
85
+ def _format_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
86
+ """Render ``headers``/``rows`` as a bordered ASCII table with a title.
87
+
88
+ A tiny reimplementation of the ``rich.Table`` output the CLI used to
89
+ produce: top/bottom title rule, a header row underlined with ``-``, and
90
+ each data row on its own line. Columns are sized to the widest cell.
91
+ """
92
+ cols = len(headers)
93
+ widths = [len(h) for h in headers]
94
+ for row in rows:
95
+ for c, cell in enumerate(row[:cols]):
96
+ widths[c] = max(widths[c], len(cell))
97
+
98
+ def _border(left: str, fill: str, right: str) -> str:
99
+ return left + fill + right
100
+
101
+ sep = _border("+", "+".join("-" * (w + 2) for w in widths), "+")
102
+
103
+ lines: list[str] = []
104
+ if title:
105
+ lines.append(_wrap("bold", title))
106
+ lines.append(sep)
107
+ header_cells = " | ".join(h.ljust(widths[c]) for c, h in enumerate(headers))
108
+ lines.append(f"| {header_cells} |")
109
+ lines.append(sep)
110
+ for row in rows:
111
+ cells = " | ".join(
112
+ str(row[c]).ljust(widths[c]) if c < len(row) else "".ljust(widths[c])
113
+ for c in range(cols)
114
+ )
115
+ lines.append(f"| {cells} |")
116
+ lines.append(sep)
117
+ return "\n".join(lines)
cds/core/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Core data models for CDS."""
2
+
3
+ from cds.core.models import Domain, Hypothesis, HypothesisStatus
4
+
5
+ __all__ = ["Domain", "Hypothesis", "HypothesisStatus"]
cds/core/_numeric.py ADDED
@@ -0,0 +1,92 @@
1
+ """Central numeric constants for CDS solvers and numerical kernels.
2
+
3
+ These replace scattered magic numbers (``1e-15`` near-zero guards, ``1e-7``
4
+ finite-difference steps, ``0.01`` learning rates, ``-1e9`` masking sentinels)
5
+ with named, documented constants. The values mirror the literals they replace
6
+ so behavior is unchanged; only readability and consistency improve.
7
+
8
+ Notes
9
+ -----
10
+ - ``EPSILON`` is derived from :func:`math.ulp` rather than hardcoded, so it
11
+ reflects the actual machine epsilon on the running interpreter.
12
+ - Module-local algorithm constants (e.g. the Lanczos ``cof`` coefficients and
13
+ ``_MAXIT`` / ``_EPS`` / ``_FPMIN`` in ``stats.hypothesis_tests``) stay where
14
+ they are: they are intrinsic to a specific Numerical-Recipes routine, not
15
+ shared tuning knobs.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+
22
+ # Machine epsilon of the running interpreter (~2.220446e-16 on CPython/IEEE-754).
23
+ # Use this instead of the legacy hardcoded ``1e-15`` singularity guards.
24
+ EPSILON: float = math.ulp(1.0)
25
+
26
+ # Near-zero threshold for singularity / divide-by-zero guards
27
+ # (pivot checks, zero-vector guards, variance floors). Kept at ``1e-15`` to
28
+ # preserve the exact behavior of the original guards.
29
+ NEAR_ZERO: float = 1e-15
30
+
31
+ # Default step size for central finite-difference gradient estimates.
32
+ DEFAULT_FD_STEP: float = 1e-7
33
+
34
+ # Generic convergence tolerance for iterative solvers (gradient descent,
35
+ # Adam, golden-section line search).
36
+ DEFAULT_TOLERANCE: float = 1e-8
37
+
38
+ # Newton-Raphson root-finding tolerance (``abs(f(x)) < tol``).
39
+ NEWTON_TOLERANCE: float = 1e-10
40
+
41
+ # Base step for the Newton-Raphson numerical derivative.
42
+ NEWTON_DERIVATIVE_STEP: float = 1e-5
43
+
44
+ # RK45 adaptive integrator defaults.
45
+ RK45_DEFAULT_ATOL: float = 1e-6
46
+ RK45_DEFAULT_RTOL: float = 1e-3
47
+ RK45_DEFAULT_DT: float = 0.01
48
+
49
+ # Adaptive-step shrink / grow factors for the RK45 controller.
50
+ RK45_STEP_SHRINK: float = 0.1
51
+ RK45_STEP_SAFETY: float = 0.9
52
+ RK45_STEP_GROW: float = 10.0
53
+
54
+ # Loop-termination epsilon for ODE time stepping (``while t < t_end - eps``).
55
+ LOOP_EPSILON: float = 1e-12
56
+
57
+ # Near-minimum positive float (Numerical Recipes ``FPMIN``), used to avoid
58
+ # underflow in continued-fraction / series evaluations.
59
+ NEAR_MIN_FLOAT: float = 1e-300
60
+
61
+ # Adam-family optimizer defaults (Kingma & Ba 2015). Unifies the defaults
62
+ # previously split between ``optimization.minimize`` (lr=0.01) and
63
+ # ``nlp.optim`` (lr=1e-3) — both now read ``ADAM_DEFAULT_LR``.
64
+ ADAM_DEFAULT_LR: float = 1e-3
65
+ ADAM_DEFAULT_BETAS: tuple[float, float] = (0.9, 0.999)
66
+ ADAM_DEFAULT_EPS: float = 1e-8
67
+
68
+ # SGD learning rate default.
69
+ SGD_DEFAULT_LR: float = 0.01
70
+
71
+ # MLP / plain gradient-descent learning rate default.
72
+ GD_DEFAULT_LR: float = 0.01
73
+
74
+ # Layer normalization variance-floor epsilon.
75
+ LAYERNORM_EPS: float = 1e-5
76
+
77
+ # Sampling temperature divide-by-zero guard.
78
+ SAMPLING_TEMPERATURE_EPS: float = 1e-8
79
+
80
+ # Softmax sampling divide-by-zero guard (legacy ``1e-8``).
81
+ SOFTMAX_TEMPERATURE_EPS: float = 1e-8
82
+
83
+ # Masked-fill sentinel replacing the legacy ``-1e9`` literal in NLP attention
84
+ # masks. ``-inf`` propagates to a true zero after softmax; ``-1e9`` only
85
+ # underflows to ~0, leaving a small residual probability.
86
+ NEG_INF_MASK: float = float("-inf")
87
+
88
+ # Entanglement (concurrence) detection threshold.
89
+ CONCURRENCE_THRESHOLD: float = 1e-9
90
+
91
+ # Plotting normalization floor (avoids division by zero on flat signals).
92
+ PLOT_NORMALIZATION_FLOOR: float = 1e-10
cds/core/models.py ADDED
@@ -0,0 +1,166 @@
1
+ """Core data models for CDS.
2
+
3
+ The data models here are plain :mod:`dataclasses` — no third-party validation
4
+ library. Field constraints that previously relied on ``pydantic`` (the
5
+ ``[0.0, 1.0]`` confidence range, enum coercion of ``domain``) are enforced in
6
+ ``Hypothesis.__post_init__``, which raises :class:`ValueError` on violations.
7
+ This keeps the whole ``cds`` package zero-dependency at runtime while
8
+ preserving the construction contract the test suite pins down.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+
19
+ class Domain(str, Enum):
20
+ """Broad scientific domains supported by CDS."""
21
+
22
+ PHYSICS = "physics"
23
+ COSMOLOGY = "cosmology"
24
+ MATHEMATICS = "mathematics"
25
+ BIOLOGY = "biology"
26
+ CHEMISTRY = "chemistry"
27
+ GENERAL_SCIENCE = "general_science"
28
+
29
+
30
+ class HypothesisStatus(str, Enum):
31
+ """Lifecycle states for a Hypothesis."""
32
+
33
+ NEW = "new"
34
+ REFINED = "refined"
35
+ CRITIQUED = "critiqued"
36
+ TESTABLE = "testable"
37
+ VALIDATED = "validated"
38
+ REJECTED = "rejected"
39
+ ARCHIVED = "archived"
40
+
41
+
42
+ @dataclass
43
+ class Hypothesis:
44
+ """A scientific hypothesis with metadata and traceability.
45
+
46
+ Attributes:
47
+ id: Unique identifier.
48
+ statement: The core hypothesis statement.
49
+ domain: Scientific domain; accepts a :class:`Domain` or its string value.
50
+ research_question: The research question this hypothesis addresses.
51
+ rationale: Optional reasoning behind the hypothesis.
52
+ assumptions: Preconditions assumed to hold.
53
+ predictions: Testable consequences of the hypothesis.
54
+ status: Current point in the hypothesis lifecycle.
55
+ confidence: Subjective belief in the hypothesis, in the closed
56
+ interval ``[0.0, 1.0]``.
57
+ created_at: Timezone-aware UTC timestamp of creation.
58
+ tags: Free-form categorical tags.
59
+ sources: References or retrieval sources backing the hypothesis.
60
+ metadata: Arbitrary string-keyed/string-valued extra information.
61
+ """
62
+
63
+ id: str
64
+ statement: str
65
+ domain: Domain
66
+ research_question: str
67
+ rationale: str | None = None
68
+ assumptions: list[str] = field(default_factory=list)
69
+ predictions: list[str] = field(default_factory=list)
70
+ status: HypothesisStatus = HypothesisStatus.NEW
71
+ confidence: float = 0.5
72
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
73
+ tags: list[str] = field(default_factory=list)
74
+ sources: list[str] = field(default_factory=list)
75
+ metadata: dict[str, str] = field(default_factory=dict)
76
+
77
+ def __post_init__(self) -> None:
78
+ """Coerce ``domain`` to its enum and validate ``confidence`` range.
79
+
80
+ Mirrors the coercion/validation pydantic performed previously: a
81
+ bare string like ``"physics"`` is accepted and converted to
82
+ :class:`Domain.PHYSICS`; ``confidence`` must lie in ``[0.0, 1.0]``.
83
+ Unknown values raise ``ValueError`` (matching the old
84
+ ``ValidationError`` behaviour).
85
+ """
86
+ # Coerce string values into the enum members so callers can pass either
87
+ # ``Domain.PHYSICS`` or the bare string ``"physics"``. This mirrors the
88
+ # coercion pydantic performed previously; an unknown value raises
89
+ # ValueError, matching the old ValidationError behaviour.
90
+ if not isinstance(self.domain, Domain):
91
+ try:
92
+ self.domain = Domain(self.domain)
93
+ except ValueError as exc:
94
+ raise ValueError(f"{self.domain!r} is not a valid Domain") from exc
95
+
96
+ # The closed interval [0.0, 1.0] is the only constraint on confidence.
97
+ # Out-of-range values raise ValueError, matching the former pydantic
98
+ # ``ge=0.0, le=1.0`` contract that the test suite pins down.
99
+ if not 0.0 <= self.confidence <= 1.0:
100
+ raise ValueError(f"confidence must be in [0.0, 1.0], got {self.confidence}")
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ """Serialize this hypothesis to a JSON-friendly dict.
104
+
105
+ Mirrors the previous ``pydantic`` ``model_dump(mode="json")`` output:
106
+ enums become their string ``value`` and ``created_at`` is rendered as
107
+ an ISO-8601 string, so the result is directly :func:`json.dumps`-able.
108
+ """
109
+ return {
110
+ "id": self.id,
111
+ "statement": self.statement,
112
+ "domain": self.domain.value,
113
+ "research_question": self.research_question,
114
+ "rationale": self.rationale,
115
+ "assumptions": list(self.assumptions),
116
+ "predictions": list(self.predictions),
117
+ "status": self.status.value,
118
+ "confidence": self.confidence,
119
+ "created_at": self.created_at.isoformat(),
120
+ "tags": list(self.tags),
121
+ "sources": list(self.sources),
122
+ "metadata": dict(self.metadata),
123
+ }
124
+
125
+ @classmethod
126
+ def from_dict(cls, data: dict[str, Any]) -> Hypothesis:
127
+ """Rebuild a :class:`Hypothesis` from :meth:`to_dict` output.
128
+
129
+ Completes the JSON round trip: enum strings are coerced back into
130
+ their :class:`Domain` / :class:`HypothesisStatus` members and
131
+ ``created_at`` is parsed from its ISO-8601 rendering. Missing
132
+ optional keys fall back to the same defaults as the constructor, so
133
+ partially-specified records load cleanly. Unknown ``status`` values
134
+ raise ``ValueError``.
135
+ """
136
+ payload = dict(data) # copy so we never mutate the caller's dict
137
+ created_at = payload.get("created_at")
138
+ if isinstance(created_at, str):
139
+ payload["created_at"] = datetime.fromisoformat(created_at)
140
+ status = payload.get("status")
141
+ if status is not None and not isinstance(status, HypothesisStatus):
142
+ payload["status"] = HypothesisStatus(status)
143
+ return cls(**payload)
144
+
145
+ def to_markdown(self) -> str:
146
+ """Render this hypothesis as a structured Markdown document."""
147
+ lines = [
148
+ f"# Hypothesis: {self.id}",
149
+ "",
150
+ f"**Statement**: {self.statement}",
151
+ "",
152
+ f"**Domain**: {self.domain.value}",
153
+ f"**Research Question**: {self.research_question}",
154
+ f"**Status**: {self.status.value} | **Confidence**: {self.confidence:.2f}",
155
+ "",
156
+ ]
157
+ if self.rationale:
158
+ lines += ["## Rationale", self.rationale, ""]
159
+ if self.assumptions:
160
+ lines += ["## Assumptions"] + [f"- {a}" for a in self.assumptions] + [""]
161
+ if self.predictions:
162
+ preds = [f"- {p}" for p in self.predictions]
163
+ lines += ["## Predictions / Testable Consequences"] + preds + [""]
164
+ if self.tags:
165
+ lines += [f"**Tags**: {', '.join(self.tags)}"]
166
+ return "\n".join(lines)
@@ -0,0 +1,30 @@
1
+ """Data loading, analysis and visualization helpers."""
2
+
3
+ from cds.data_analysis.dataset import DataSet
4
+ from cds.data_analysis.loader import DataTable, load_csv
5
+ from cds.data_analysis.transform import moving_average, normalize, z_score
6
+ from cds.data_analysis.viz import plot_bar, plot_line
7
+
8
+ __all__ = [
9
+ "load_csv",
10
+ "DataTable",
11
+ "DataSet",
12
+ "normalize",
13
+ "z_score",
14
+ "moving_average",
15
+ "plot_bar",
16
+ "plot_line",
17
+ ]
18
+
19
+
20
+ def __getattr__(name: str): # type: ignore[no-untyped-def]
21
+ """Lazily expose the optional pandas bridge.
22
+
23
+ ``pandas_io`` imports pandas at call time, so it is only resolved on first
24
+ access — keeping the core import zero-dependency.
25
+ """
26
+ if name in {"to_dataframe", "from_dataframe"}:
27
+ from cds.data_analysis import pandas_io
28
+
29
+ return getattr(pandas_io, name)
30
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")