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.
- cds/__init__.py +102 -0
- cds/__main__.py +8 -0
- cds/_version.py +10 -0
- cds/cli/__init__.py +104 -0
- cds/cli/__main__.py +8 -0
- cds/cli/_handlers.py +405 -0
- cds/cli/_parser.py +156 -0
- cds/cli/_style.py +117 -0
- cds/core/__init__.py +5 -0
- cds/core/_numeric.py +92 -0
- cds/core/models.py +166 -0
- cds/data_analysis/__init__.py +30 -0
- cds/data_analysis/dataset.py +122 -0
- cds/data_analysis/loader.py +75 -0
- cds/data_analysis/pandas_io.py +108 -0
- cds/data_analysis/transform.py +36 -0
- cds/data_analysis/viz.py +88 -0
- cds/diffeq/__init__.py +29 -0
- cds/diffeq/_implicit.py +349 -0
- cds/diffeq/solvers.py +312 -0
- cds/graph/__init__.py +31 -0
- cds/graph/algorithms.py +399 -0
- cds/hypothesis/__init__.py +48 -0
- cds/hypothesis/evaluator.py +369 -0
- cds/hypothesis/generator.py +275 -0
- cds/knowledge/__init__.py +41 -0
- cds/knowledge/graph.py +489 -0
- cds/knowledge/notes.py +231 -0
- cds/knowledge/retrieval.py +167 -0
- cds/math_utils/__init__.py +41 -0
- cds/math_utils/calculus.py +40 -0
- cds/math_utils/linalg.py +461 -0
- cds/math_utils/special.py +167 -0
- cds/ml/__init__.py +25 -0
- cds/ml/clustering.py +190 -0
- cds/ml/decomposition.py +172 -0
- cds/ml/linear_models.py +225 -0
- cds/ml/neighbors.py +162 -0
- cds/ml/neural.py +204 -0
- cds/ml/preprocessing.py +131 -0
- cds/ml/tree.py +202 -0
- cds/modeling/__init__.py +40 -0
- cds/modeling/_base.py +210 -0
- cds/modeling/_nodes.py +526 -0
- cds/modeling/expression.py +81 -0
- cds/modeling/model.py +134 -0
- cds/modeling/solver.py +176 -0
- cds/montecarlo/__init__.py +21 -0
- cds/montecarlo/methods.py +272 -0
- cds/nlp/__init__.py +170 -0
- cds/nlp/attention.py +285 -0
- cds/nlp/autograd/__init__.py +70 -0
- cds/nlp/autograd/_grad.py +120 -0
- cds/nlp/autograd/ops.py +133 -0
- cds/nlp/autograd/tensor.py +298 -0
- cds/nlp/bpe.py +449 -0
- cds/nlp/data.py +70 -0
- cds/nlp/embed.py +190 -0
- cds/nlp/layers.py +259 -0
- cds/nlp/model.py +456 -0
- cds/nlp/optim.py +163 -0
- cds/nlp/training.py +167 -0
- cds/nlp/viz.py +254 -0
- cds/numerical_integration/__init__.py +31 -0
- cds/numerical_integration/quadrature.py +480 -0
- cds/optimization/__init__.py +18 -0
- cds/optimization/_metaheuristics.py +287 -0
- cds/optimization/minimize.py +425 -0
- cds/plot/__init__.py +49 -0
- cds/plot/_backend.py +32 -0
- cds/plot/charts.py +462 -0
- cds/probability/__init__.py +55 -0
- cds/probability/_advanced.py +283 -0
- cds/probability/distributions.py +236 -0
- cds/py.typed +0 -0
- cds/quantum/__init__.py +51 -0
- cds/quantum/circuit.py +92 -0
- cds/quantum/multi_qubit.py +255 -0
- cds/quantum/simulator.py +38 -0
- cds/scientific/__init__.py +34 -0
- cds/scientific/constants.py +35 -0
- cds/scientific/formulas.py +116 -0
- cds/signals/__init__.py +43 -0
- cds/signals/filters.py +421 -0
- cds/signals/processing.py +224 -0
- cds/stats/__init__.py +83 -0
- cds/stats/_distributions.py +102 -0
- cds/stats/descriptive.py +172 -0
- cds/stats/hypothesis_tests.py +403 -0
- cds/stats/nonparametric.py +131 -0
- cds/stats/regression.py +51 -0
- cds/stats/time_series.py +408 -0
- scientific_computing_system-1.6.2.dist-info/METADATA +741 -0
- scientific_computing_system-1.6.2.dist-info/RECORD +97 -0
- scientific_computing_system-1.6.2.dist-info/WHEEL +4 -0
- scientific_computing_system-1.6.2.dist-info/entry_points.txt +2 -0
- scientific_computing_system-1.6.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Pure Python DataFrame-like structure for structured data analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
# A cell value in a DataSet row. Tabular data here is expected to be scalar
|
|
9
|
+
# primitives — numbers, strings, booleans, or null — rather than nested
|
|
10
|
+
# containers, which keeps column extraction and aggregation type-safe.
|
|
11
|
+
Scalar: TypeAlias = int | float | str | bool | None
|
|
12
|
+
|
|
13
|
+
# A single row maps column name -> cell value.
|
|
14
|
+
Row: TypeAlias = dict[str, Scalar]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataSet:
|
|
18
|
+
"""A lightweight, pure Python 'DataFrame' for structured data.
|
|
19
|
+
|
|
20
|
+
Data is stored internally as a list of dictionaries where keys are column names.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, data: list[Row]):
|
|
24
|
+
"""Store ``data`` and derive column names from the first row's keys."""
|
|
25
|
+
self.data = data
|
|
26
|
+
self._columns = list(data[0].keys()) if data else []
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def columns(self) -> list[str]:
|
|
30
|
+
"""Return the list of column names."""
|
|
31
|
+
return self._columns
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def shape(self) -> tuple[int, int]:
|
|
35
|
+
"""Return (rows, columns) tuple."""
|
|
36
|
+
return len(self.data), len(self._columns)
|
|
37
|
+
|
|
38
|
+
def __len__(self) -> int:
|
|
39
|
+
"""Return the number of rows."""
|
|
40
|
+
return len(self.data)
|
|
41
|
+
|
|
42
|
+
def __getitem__(self, idx: int) -> Row:
|
|
43
|
+
"""Return the row at integer index ``idx``."""
|
|
44
|
+
return self.data[idx]
|
|
45
|
+
|
|
46
|
+
def column(self, name: str) -> list[Scalar]:
|
|
47
|
+
"""Extract a single column as a list."""
|
|
48
|
+
if name not in self._columns:
|
|
49
|
+
raise ValueError(f"Column '{name}' not found. Available: {self._columns}")
|
|
50
|
+
return [row[name] for row in self.data]
|
|
51
|
+
|
|
52
|
+
def filter(self, predicate: Callable[[Row], bool]) -> DataSet:
|
|
53
|
+
"""Filter the dataset based on a predicate function."""
|
|
54
|
+
filtered_data = [row for row in self.data if predicate(row)]
|
|
55
|
+
return DataSet(filtered_data)
|
|
56
|
+
|
|
57
|
+
def head(self, n: int = 5) -> DataSet:
|
|
58
|
+
"""Return the first n rows."""
|
|
59
|
+
return DataSet(self.data[:n])
|
|
60
|
+
|
|
61
|
+
def tail(self, n: int = 5) -> DataSet:
|
|
62
|
+
"""Return the last n rows."""
|
|
63
|
+
return DataSet(self.data[-n:])
|
|
64
|
+
|
|
65
|
+
def select(self, *names: str) -> DataSet:
|
|
66
|
+
"""Select a subset of columns."""
|
|
67
|
+
for name in names:
|
|
68
|
+
if name not in self._columns:
|
|
69
|
+
raise ValueError(f"Column '{name}' not found.")
|
|
70
|
+
|
|
71
|
+
new_data = [{name: row[name] for name in names} for row in self.data]
|
|
72
|
+
return DataSet(new_data)
|
|
73
|
+
|
|
74
|
+
def group_by(self, column_name: str) -> DataGroup:
|
|
75
|
+
"""Group data by a specific column for aggregation."""
|
|
76
|
+
if column_name not in self._columns:
|
|
77
|
+
raise ValueError(f"Column '{column_name}' not found.")
|
|
78
|
+
|
|
79
|
+
groups: dict[Scalar, list[Row]] = {}
|
|
80
|
+
for row in self.data:
|
|
81
|
+
key = row[column_name]
|
|
82
|
+
if key not in groups:
|
|
83
|
+
groups[key] = []
|
|
84
|
+
groups[key].append(row)
|
|
85
|
+
|
|
86
|
+
return DataGroup(groups, column_name)
|
|
87
|
+
|
|
88
|
+
def to_list(self) -> list[Row]:
|
|
89
|
+
"""Export data as a list of dictionaries."""
|
|
90
|
+
return [row.copy() for row in self.data]
|
|
91
|
+
|
|
92
|
+
def __repr__(self) -> str:
|
|
93
|
+
"""Return a compact ``DataSet(rows=..., cols=...)`` summary."""
|
|
94
|
+
if not self.data:
|
|
95
|
+
return "DataSet(empty)"
|
|
96
|
+
return f"DataSet(rows={len(self.data)}, cols={len(self._columns)})"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DataGroup:
|
|
100
|
+
"""Helper class for grouped data aggregations."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, groups: dict[Scalar, list[Row]], group_col: str):
|
|
103
|
+
"""Store the grouped rows and the name of the grouping column."""
|
|
104
|
+
self.groups = groups
|
|
105
|
+
self.group_col = group_col
|
|
106
|
+
|
|
107
|
+
def mean(self, numeric_col: str) -> dict[Scalar, float]:
|
|
108
|
+
"""Calculate the mean of a numeric column for each group."""
|
|
109
|
+
result: dict[Scalar, float] = {}
|
|
110
|
+
for key, rows in self.groups.items():
|
|
111
|
+
values: list[float] = [
|
|
112
|
+
float(v) for row in rows if isinstance((v := row.get(numeric_col)), (int, float))
|
|
113
|
+
]
|
|
114
|
+
if values:
|
|
115
|
+
result[key] = sum(values) / len(values)
|
|
116
|
+
else:
|
|
117
|
+
result[key] = 0.0
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def count(self) -> dict[Scalar, int]:
|
|
121
|
+
"""Count the number of rows in each group."""
|
|
122
|
+
return {key: len(rows) for key, rows in self.groups.items()}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Simple CSV loader — no pandas dependency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DataTable:
|
|
12
|
+
"""In-memory tabular data: a header row plus a list of string rows."""
|
|
13
|
+
|
|
14
|
+
headers: list[str] = field(default_factory=list)
|
|
15
|
+
rows: list[list[str]] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def n_rows(self) -> int:
|
|
19
|
+
"""Number of data rows (excluding the header)."""
|
|
20
|
+
return len(self.rows)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def n_cols(self) -> int:
|
|
24
|
+
"""Number of columns (i.e. number of header entries)."""
|
|
25
|
+
return len(self.headers)
|
|
26
|
+
|
|
27
|
+
def column(self, name: str) -> list[str]:
|
|
28
|
+
"""Return all values in the column identified by `name`."""
|
|
29
|
+
idx = self.headers.index(name)
|
|
30
|
+
return [row[idx] for row in self.rows]
|
|
31
|
+
|
|
32
|
+
def column_as_float(self, name: str) -> list[float]:
|
|
33
|
+
"""Return a column as floats; raises ValueError if a cell is non-numeric."""
|
|
34
|
+
return [float(v) for v in self.column(name)]
|
|
35
|
+
|
|
36
|
+
def head(self, n: int = 5) -> list[list[str]]:
|
|
37
|
+
"""Return the first `n` rows (default 5) for quick inspection."""
|
|
38
|
+
return self.rows[:n]
|
|
39
|
+
|
|
40
|
+
def describe(self) -> dict[str, dict[str, float]]:
|
|
41
|
+
"""Quick summary stats for numeric columns."""
|
|
42
|
+
from cds.stats.descriptive import mean, median, stdev
|
|
43
|
+
|
|
44
|
+
result: dict[str, dict[str, float]] = {}
|
|
45
|
+
for h in self.headers:
|
|
46
|
+
try:
|
|
47
|
+
vals = self.column_as_float(h)
|
|
48
|
+
result[h] = {
|
|
49
|
+
"count": len(vals),
|
|
50
|
+
"mean": mean(vals),
|
|
51
|
+
"std": stdev(vals),
|
|
52
|
+
"min": min(vals),
|
|
53
|
+
"median": median(vals),
|
|
54
|
+
"max": max(vals),
|
|
55
|
+
}
|
|
56
|
+
except (ValueError, TypeError):
|
|
57
|
+
# Non-numeric column — skip aggregation, keep prior result.
|
|
58
|
+
pass
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_csv(path: str | Path) -> DataTable:
|
|
63
|
+
"""Load a CSV file into a DataTable.
|
|
64
|
+
|
|
65
|
+
The first row is treated as headers; remaining rows are stored as strings.
|
|
66
|
+
Raises FileNotFoundError if `path` does not exist.
|
|
67
|
+
"""
|
|
68
|
+
p = Path(path)
|
|
69
|
+
if not p.exists():
|
|
70
|
+
raise FileNotFoundError(f"no such file: {p}")
|
|
71
|
+
with open(p, newline="") as f:
|
|
72
|
+
reader = csv.reader(f)
|
|
73
|
+
headers = next(reader)
|
|
74
|
+
rows = list(reader)
|
|
75
|
+
return DataTable(headers=headers, rows=rows)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Pandas interoperability bridge for :class:`cds.data_analysis.DataSet`.
|
|
2
|
+
|
|
3
|
+
This module is **optional**: pandas is imported lazily, so the core package
|
|
4
|
+
remains zero-dependency. Install the extra with ``pip install ".[pandas]"``
|
|
5
|
+
(or ``[all]``). Importing this module without pandas installed raises a clear
|
|
6
|
+
``ImportError`` with install guidance.
|
|
7
|
+
|
|
8
|
+
The bridge converts between CDS's pure-Python :class:`DataSet` (a list of
|
|
9
|
+
row dicts) and a pandas ``DataFrame``, letting users who already live in the
|
|
10
|
+
pandas ecosystem move data in both directions without rewriting pipelines.
|
|
11
|
+
|
|
12
|
+
Design notes:
|
|
13
|
+
- Conversions are lossless for the scalar types :class:`DataSet` supports
|
|
14
|
+
(``int | float | str | bool | None``).
|
|
15
|
+
- ``None`` cells map to ``NaN`` in pandas and back, matching pandas convention.
|
|
16
|
+
- Column order is preserved from the source on both directions.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from cds.data_analysis.dataset import DataSet, Row
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
# Imported only under TYPE_CHECKING so the module imports cleanly when
|
|
27
|
+
# pandas is absent; the real import happens inside the functions.
|
|
28
|
+
import pandas as pd
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_PANDAS_INSTALL_HINT = (
|
|
32
|
+
"pandas is an optional dependency. Install it with "
|
|
33
|
+
"`pip install scientific-computing-system[pandas]` "
|
|
34
|
+
"(or `pip install pandas`)."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _require_pandas() -> Any:
|
|
39
|
+
"""Import and return the ``pandas`` module, raising a helpful error if absent."""
|
|
40
|
+
try:
|
|
41
|
+
import pandas as pd
|
|
42
|
+
except ImportError as exc: # pragma: no cover - exercised only without pandas
|
|
43
|
+
raise ImportError(_PANDAS_INSTALL_HINT) from exc
|
|
44
|
+
return pd
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_dataframe(ds: DataSet) -> pd.DataFrame:
|
|
48
|
+
"""Convert a :class:`DataSet` to a pandas ``DataFrame``.
|
|
49
|
+
|
|
50
|
+
Each row dict becomes a DataFrame row; column order follows
|
|
51
|
+
``ds.columns``. ``None`` values are preserved as-is (pandas renders them
|
|
52
|
+
as ``NaN`` in numeric columns).
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ds: a CDS :class:`DataSet`.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A pandas ``DataFrame`` with the same rows and columns.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ImportError: if pandas is not installed.
|
|
62
|
+
"""
|
|
63
|
+
pandas_module = _require_pandas()
|
|
64
|
+
rows = ds.to_list()
|
|
65
|
+
columns = ds.columns
|
|
66
|
+
# The DataFrame constructor is typed as returning Any in the stubs; assign
|
|
67
|
+
# to an annotated local so the return type is pinned without a cast.
|
|
68
|
+
df: pd.DataFrame = pandas_module.DataFrame(rows, columns=columns if columns else None)
|
|
69
|
+
return df
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def from_dataframe(df: pd.DataFrame) -> DataSet:
|
|
73
|
+
"""Convert a pandas ``DataFrame`` to a :class:`DataSet`.
|
|
74
|
+
|
|
75
|
+
Each DataFrame row becomes a row dict keyed by column name. pandas
|
|
76
|
+
``NaN`` values are converted to ``None`` to match :class:`DataSet`'s
|
|
77
|
+
scalar type contract.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
df: a pandas ``DataFrame``.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A CDS :class:`DataSet` with the same data.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ImportError: if pandas is not installed.
|
|
87
|
+
"""
|
|
88
|
+
pandas_module = _require_pandas()
|
|
89
|
+
rows: list[Row] = []
|
|
90
|
+
# .to_dict(orient="records") yields plain dicts; NaN -> NaN (float).
|
|
91
|
+
# We normalize NaN to None to honor the DataSet scalar contract.
|
|
92
|
+
raw_records = df.to_dict(orient="records")
|
|
93
|
+
for record in raw_records:
|
|
94
|
+
row: Row = {}
|
|
95
|
+
for key, value in record.items():
|
|
96
|
+
# pd.isna handles both scalar NaN and None, but raises on arrays.
|
|
97
|
+
# Values here are scalars from a DataFrame cell.
|
|
98
|
+
try:
|
|
99
|
+
is_na = bool(pandas_module.isna(value))
|
|
100
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
101
|
+
# pd.isna raises only on array-like inputs. DataSet's Row
|
|
102
|
+
# contract restricts cells to scalars (int|float|str|bool|None),
|
|
103
|
+
# so this branch is unreachable for conforming data; kept as a
|
|
104
|
+
# guard against malformed/foreign rows slipping through.
|
|
105
|
+
is_na = value is None
|
|
106
|
+
row[str(key)] = None if is_na else value
|
|
107
|
+
rows.append(row)
|
|
108
|
+
return DataSet(rows)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Data transformations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cds.stats.descriptive import mean, stdev
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize(data: list[float]) -> list[float]:
|
|
9
|
+
"""Min-max normalization to [0, 1]."""
|
|
10
|
+
lo, hi = min(data), max(data)
|
|
11
|
+
rng = hi - lo
|
|
12
|
+
if rng == 0:
|
|
13
|
+
return [0.0] * len(data)
|
|
14
|
+
return [(x - lo) / rng for x in data]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def z_score(data: list[float]) -> list[float]:
|
|
18
|
+
"""Standardize to mean=0, std=1."""
|
|
19
|
+
m = mean(data)
|
|
20
|
+
s = stdev(data)
|
|
21
|
+
if s == 0:
|
|
22
|
+
return [0.0] * len(data)
|
|
23
|
+
return [(x - m) / s for x in data]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def moving_average(data: list[float], window: int = 3) -> list[float]:
|
|
27
|
+
"""Trailing moving average with the given window size (>= 1)."""
|
|
28
|
+
|
|
29
|
+
if window < 1:
|
|
30
|
+
raise ValueError("window must be >= 1")
|
|
31
|
+
result = []
|
|
32
|
+
for i in range(len(data)):
|
|
33
|
+
start = max(0, i - window + 1)
|
|
34
|
+
chunk = data[start : i + 1]
|
|
35
|
+
result.append(sum(chunk) / len(chunk))
|
|
36
|
+
return result
|
cds/data_analysis/viz.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Pure Python ASCII visualization tools for structured data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def plot_bar(data: dict[str, float], title: str = "Bar Chart", width: int = 50) -> str:
|
|
7
|
+
"""Generate an ASCII bar chart from a dictionary.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
data: Mapping from label to numeric value.
|
|
11
|
+
title: Chart title.
|
|
12
|
+
width: Maximum bar width in characters.
|
|
13
|
+
"""
|
|
14
|
+
if not data:
|
|
15
|
+
return "No data to plot."
|
|
16
|
+
|
|
17
|
+
vals = list(data.values())
|
|
18
|
+
max_val = max(vals)
|
|
19
|
+
min_val = min(vals)
|
|
20
|
+
|
|
21
|
+
lines = [f"\n[bold]{title}[/]", "─" * len(title)]
|
|
22
|
+
|
|
23
|
+
# Simple normalization logic that respects 0 as a baseline
|
|
24
|
+
limit = max(abs(max_val), abs(min_val), 1e-10)
|
|
25
|
+
|
|
26
|
+
for label, val in data.items():
|
|
27
|
+
# Calculate bar length relative to the largest absolute value
|
|
28
|
+
bar_len = int((abs(val) / limit) * width)
|
|
29
|
+
if val >= 0:
|
|
30
|
+
bar = "█" * bar_len
|
|
31
|
+
suffix = f" (+{val:.2f})"
|
|
32
|
+
else:
|
|
33
|
+
# Represent negative values with a different character or notation
|
|
34
|
+
bar = "░" * bar_len
|
|
35
|
+
suffix = f" ({val:.2f})"
|
|
36
|
+
|
|
37
|
+
lines.append(f"{label:<15} | {bar}{suffix}")
|
|
38
|
+
|
|
39
|
+
return "\n".join(lines)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def plot_line(
|
|
43
|
+
y_values: list[float], title: str = "Line Plot", height: int = 10, width: int = 60
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Generate a simple ASCII line plot.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
y_values: List of numeric values.
|
|
49
|
+
title: Plot title.
|
|
50
|
+
height: Number of rows.
|
|
51
|
+
width: Number of columns (will sample data to fit).
|
|
52
|
+
"""
|
|
53
|
+
if not y_values:
|
|
54
|
+
return "No data to plot."
|
|
55
|
+
|
|
56
|
+
# Guard: width must be at least 2; otherwise stride below divides by zero.
|
|
57
|
+
eff_width = max(2, width)
|
|
58
|
+
|
|
59
|
+
# Sample/Interpolate to fit width
|
|
60
|
+
if len(y_values) > eff_width:
|
|
61
|
+
indices = [int(i * (len(y_values) - 1) / (eff_width - 1)) for i in range(eff_width)]
|
|
62
|
+
sampled = [y_values[i] for i in indices]
|
|
63
|
+
else:
|
|
64
|
+
sampled = y_values
|
|
65
|
+
eff_width = len(y_values)
|
|
66
|
+
|
|
67
|
+
max_y = max(sampled)
|
|
68
|
+
min_y = min(sampled)
|
|
69
|
+
y_range = max_y - min_y if max_y != min_y else 1.0
|
|
70
|
+
|
|
71
|
+
# Create grid
|
|
72
|
+
grid = [[" " for _ in range(eff_width)] for _ in range(height)]
|
|
73
|
+
|
|
74
|
+
for x, y in enumerate(sampled):
|
|
75
|
+
# Calculate row (inverted because row 0 is top)
|
|
76
|
+
norm_y = (y - min_y) / y_range
|
|
77
|
+
row = height - 1 - int(norm_y * (height - 1))
|
|
78
|
+
grid[row][x] = "•"
|
|
79
|
+
|
|
80
|
+
lines = [f"\n[bold]{title}[/]", "─" * len(title)]
|
|
81
|
+
for grid_row in grid:
|
|
82
|
+
lines.append("".join(grid_row))
|
|
83
|
+
|
|
84
|
+
min_y_str = f"{min_y:.2f}"
|
|
85
|
+
max_y_str = f"{max_y:.2f}"
|
|
86
|
+
|
|
87
|
+
lines.append(f"min: {min_y_str:<{eff_width // 2}}max: {max_y_str:>{eff_width // 2}}")
|
|
88
|
+
return "\n".join(lines)
|
cds/diffeq/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Ordinary differential equation solvers — Euler, RK4, RK45, implicit methods."""
|
|
2
|
+
|
|
3
|
+
from cds.diffeq._implicit import (
|
|
4
|
+
backward_euler,
|
|
5
|
+
backward_euler_system,
|
|
6
|
+
trapezoid_method,
|
|
7
|
+
trapezoid_method_system,
|
|
8
|
+
)
|
|
9
|
+
from cds.diffeq.solvers import (
|
|
10
|
+
ODESolution,
|
|
11
|
+
euler_method,
|
|
12
|
+
midpoint_method,
|
|
13
|
+
rk4,
|
|
14
|
+
rk45,
|
|
15
|
+
solve_system,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ODESolution",
|
|
20
|
+
"backward_euler",
|
|
21
|
+
"backward_euler_system",
|
|
22
|
+
"euler_method",
|
|
23
|
+
"midpoint_method",
|
|
24
|
+
"rk4",
|
|
25
|
+
"rk45",
|
|
26
|
+
"solve_system",
|
|
27
|
+
"trapezoid_method",
|
|
28
|
+
"trapezoid_method_system",
|
|
29
|
+
]
|