bossanova 0.1.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bossanova-0.1.0.dev0/.gitignore +69 -0
- bossanova-0.1.0.dev0/LICENSE +21 -0
- bossanova-0.1.0.dev0/PKG-INFO +27 -0
- bossanova-0.1.0.dev0/README.md +3 -0
- bossanova-0.1.0.dev0/bossanova/__init__.py +86 -0
- bossanova-0.1.0.dev0/bossanova/_backend.py +183 -0
- bossanova-0.1.0.dev0/bossanova/_config.py +48 -0
- bossanova-0.1.0.dev0/bossanova/_parser/__init__.py +53 -0
- bossanova-0.1.0.dev0/bossanova/_parser/expr.py +148 -0
- bossanova-0.1.0.dev0/bossanova/_parser/parser.py +260 -0
- bossanova-0.1.0.dev0/bossanova/_parser/scanner.py +240 -0
- bossanova-0.1.0.dev0/bossanova/_parser/token.py +30 -0
- bossanova-0.1.0.dev0/bossanova/_utils.py +37 -0
- bossanova-0.1.0.dev0/bossanova/data/__init__.py +115 -0
- bossanova-0.1.0.dev0/bossanova/data/advertising.csv +201 -0
- bossanova-0.1.0.dev0/bossanova/data/chickweight.csv +579 -0
- bossanova-0.1.0.dev0/bossanova/data/credit.csv +401 -0
- bossanova-0.1.0.dev0/bossanova/data/gammas.csv +6001 -0
- bossanova-0.1.0.dev0/bossanova/data/mtcars.csv +33 -0
- bossanova-0.1.0.dev0/bossanova/data/penguins.csv +345 -0
- bossanova-0.1.0.dev0/bossanova/data/poker.csv +301 -0
- bossanova-0.1.0.dev0/bossanova/data/sleep.csv +181 -0
- bossanova-0.1.0.dev0/bossanova/data/titanic.csv +892 -0
- bossanova-0.1.0.dev0/bossanova/data/titanic_test.csv +419 -0
- bossanova-0.1.0.dev0/bossanova/data/titanic_train.csv +892 -0
- bossanova-0.1.0.dev0/bossanova/formula/__init__.py +94 -0
- bossanova-0.1.0.dev0/bossanova/formula/contrasts.py +555 -0
- bossanova-0.1.0.dev0/bossanova/formula/design.py +1976 -0
- bossanova-0.1.0.dev0/bossanova/formula/encoding.py +298 -0
- bossanova-0.1.0.dev0/bossanova/formula/random_effects.py +131 -0
- bossanova-0.1.0.dev0/bossanova/formula/transforms.py +577 -0
- bossanova-0.1.0.dev0/bossanova/formula/z_matrix.py +538 -0
- bossanova-0.1.0.dev0/bossanova/grammar/__init__.py +204 -0
- bossanova-0.1.0.dev0/bossanova/grammar/assuming.py +227 -0
- bossanova-0.1.0.dev0/bossanova/grammar/explain.py +610 -0
- bossanova-0.1.0.dev0/bossanova/grammar/explore.py +682 -0
- bossanova-0.1.0.dev0/bossanova/grammar/fit.py +142 -0
- bossanova-0.1.0.dev0/bossanova/grammar/infer.py +812 -0
- bossanova-0.1.0.dev0/bossanova/grammar/model.py +269 -0
- bossanova-0.1.0.dev0/bossanova/grammar/pipe.py +133 -0
- bossanova-0.1.0.dev0/bossanova/grammar/predict.py +672 -0
- bossanova-0.1.0.dev0/bossanova/grammar/results.py +2354 -0
- bossanova-0.1.0.dev0/bossanova/grammar/specs.py +984 -0
- bossanova-0.1.0.dev0/bossanova/grammar/strategies.py +1802 -0
- bossanova-0.1.0.dev0/bossanova/grammar/viz.py +162 -0
- bossanova-0.1.0.dev0/bossanova/marginal/__init__.py +81 -0
- bossanova-0.1.0.dev0/bossanova/marginal/contrasts.py +225 -0
- bossanova-0.1.0.dev0/bossanova/marginal/emm.py +428 -0
- bossanova-0.1.0.dev0/bossanova/marginal/grid.py +216 -0
- bossanova-0.1.0.dev0/bossanova/marginal/hypothesis.py +313 -0
- bossanova-0.1.0.dev0/bossanova/marginal/joint_tests.py +356 -0
- bossanova-0.1.0.dev0/bossanova/marginal/parser.py +275 -0
- bossanova-0.1.0.dev0/bossanova/marginal/slopes.py +234 -0
- bossanova-0.1.0.dev0/bossanova/models/__init__.py +16 -0
- bossanova-0.1.0.dev0/bossanova/models/base/__init__.py +6 -0
- bossanova-0.1.0.dev0/bossanova/models/base/mixed.py +1527 -0
- bossanova-0.1.0.dev0/bossanova/models/base/model.py +2905 -0
- bossanova-0.1.0.dev0/bossanova/models/display.py +189 -0
- bossanova-0.1.0.dev0/bossanova/models/glm.py +1051 -0
- bossanova-0.1.0.dev0/bossanova/models/glmer.py +1430 -0
- bossanova-0.1.0.dev0/bossanova/models/lm.py +818 -0
- bossanova-0.1.0.dev0/bossanova/models/lmer.py +1511 -0
- bossanova-0.1.0.dev0/bossanova/models/ridge.py +1139 -0
- bossanova-0.1.0.dev0/bossanova/ops/__init__.py +77 -0
- bossanova-0.1.0.dev0/bossanova/ops/_array_ops.py +259 -0
- bossanova-0.1.0.dev0/bossanova/ops/_get_ops.py +60 -0
- bossanova-0.1.0.dev0/bossanova/ops/_jax_backend.py +169 -0
- bossanova-0.1.0.dev0/bossanova/ops/_numpy_backend.py +169 -0
- bossanova-0.1.0.dev0/bossanova/ops/batching.py +129 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/__init__.py +48 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/dag_viz.py +526 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/graphs.py +386 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/identify.py +350 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/parse.py +227 -0
- bossanova-0.1.0.dev0/bossanova/ops/causal/patterns.py +362 -0
- bossanova-0.1.0.dev0/bossanova/ops/convergence.py +393 -0
- bossanova-0.1.0.dev0/bossanova/ops/diagnostics.py +241 -0
- bossanova-0.1.0.dev0/bossanova/ops/family.py +726 -0
- bossanova-0.1.0.dev0/bossanova/ops/glm_fit.py +508 -0
- bossanova-0.1.0.dev0/bossanova/ops/glmer_pirls.py +1350 -0
- bossanova-0.1.0.dev0/bossanova/ops/inference.py +333 -0
- bossanova-0.1.0.dev0/bossanova/ops/initialization.py +356 -0
- bossanova-0.1.0.dev0/bossanova/ops/lambda_builder.py +800 -0
- bossanova-0.1.0.dev0/bossanova/ops/linalg.py +374 -0
- bossanova-0.1.0.dev0/bossanova/ops/lmer_core.py +613 -0
- bossanova-0.1.0.dev0/bossanova/ops/predict.py +76 -0
- bossanova-0.1.0.dev0/bossanova/ops/ridge_fit.py +384 -0
- bossanova-0.1.0.dev0/bossanova/ops/ridge_inference.py +48 -0
- bossanova-0.1.0.dev0/bossanova/ops/rng.py +321 -0
- bossanova-0.1.0.dev0/bossanova/ops/sparse_solver.py +376 -0
- bossanova-0.1.0.dev0/bossanova/optimize/__init__.py +5 -0
- bossanova-0.1.0.dev0/bossanova/optimize/bobyqa.py +171 -0
- bossanova-0.1.0.dev0/bossanova/py.typed +0 -0
- bossanova-0.1.0.dev0/bossanova/resample/__init__.py +120 -0
- bossanova-0.1.0.dev0/bossanova/resample/core.py +412 -0
- bossanova-0.1.0.dev0/bossanova/resample/glm.py +810 -0
- bossanova-0.1.0.dev0/bossanova/resample/lm.py +1171 -0
- bossanova-0.1.0.dev0/bossanova/resample/mixed.py +976 -0
- bossanova-0.1.0.dev0/bossanova/resample/results.py +294 -0
- bossanova-0.1.0.dev0/bossanova/resample/ridge.py +476 -0
- bossanova-0.1.0.dev0/bossanova/resample/utils.py +183 -0
- bossanova-0.1.0.dev0/bossanova/results/__init__.py +84 -0
- bossanova-0.1.0.dev0/bossanova/results/builders.py +1155 -0
- bossanova-0.1.0.dev0/bossanova/results/schemas.py +675 -0
- bossanova-0.1.0.dev0/bossanova/simulation/__init__.py +60 -0
- bossanova-0.1.0.dev0/bossanova/simulation/dgp/__init__.py +17 -0
- bossanova-0.1.0.dev0/bossanova/simulation/dgp/glm.py +180 -0
- bossanova-0.1.0.dev0/bossanova/simulation/dgp/glmer.py +183 -0
- bossanova-0.1.0.dev0/bossanova/simulation/dgp/lm.py +108 -0
- bossanova-0.1.0.dev0/bossanova/simulation/dgp/lmer.py +184 -0
- bossanova-0.1.0.dev0/bossanova/simulation/harness.py +341 -0
- bossanova-0.1.0.dev0/bossanova/simulation/metrics.py +149 -0
- bossanova-0.1.0.dev0/bossanova/stats/__init__.py +31 -0
- bossanova-0.1.0.dev0/bossanova/stats/compare.py +1019 -0
- bossanova-0.1.0.dev0/bossanova/stats/effect_sizes.py +180 -0
- bossanova-0.1.0.dev0/bossanova/stats/lrt.py +69 -0
- bossanova-0.1.0.dev0/bossanova/stats/satterthwaite.py +945 -0
- bossanova-0.1.0.dev0/bossanova/viz/__init__.py +80 -0
- bossanova-0.1.0.dev0/bossanova/viz/_core.py +493 -0
- bossanova-0.1.0.dev0/bossanova/viz/cognition.py +250 -0
- bossanova-0.1.0.dev0/bossanova/viz/compare.py +299 -0
- bossanova-0.1.0.dev0/bossanova/viz/dag.py +416 -0
- bossanova-0.1.0.dev0/bossanova/viz/design.py +414 -0
- bossanova-0.1.0.dev0/bossanova/viz/fit.py +313 -0
- bossanova-0.1.0.dev0/bossanova/viz/lattice.py +384 -0
- bossanova-0.1.0.dev0/bossanova/viz/layout.py +562 -0
- bossanova-0.1.0.dev0/bossanova/viz/mem.py +426 -0
- bossanova-0.1.0.dev0/bossanova/viz/params.py +233 -0
- bossanova-0.1.0.dev0/bossanova/viz/predict.py +528 -0
- bossanova-0.1.0.dev0/bossanova/viz/ranef.py +300 -0
- bossanova-0.1.0.dev0/bossanova/viz/relationships.py +209 -0
- bossanova-0.1.0.dev0/bossanova/viz/resid.py +402 -0
- bossanova-0.1.0.dev0/bossanova/viz/vif.py +302 -0
- bossanova-0.1.0.dev0/pyproject.toml +186 -0
- bossanova-0.1.0.dev0/tests/bossanova_benchmarks/bootstrap/data/README.md +31 -0
- bossanova-0.1.0.dev0/tests/bossanova_benchmarks/insteval/data/README.md +42 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
.ruff*
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
sdist/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
.venv/
|
|
8
|
+
dev
|
|
9
|
+
*.DS*
|
|
10
|
+
research/
|
|
11
|
+
plans/
|
|
12
|
+
archive/
|
|
13
|
+
.private-journal
|
|
14
|
+
|
|
15
|
+
_build/
|
|
16
|
+
docs/_build/
|
|
17
|
+
docs/performance/
|
|
18
|
+
profile*.py
|
|
19
|
+
benchmarks/
|
|
20
|
+
examples/
|
|
21
|
+
papers/
|
|
22
|
+
paper-summaries/
|
|
23
|
+
prompts/
|
|
24
|
+
experiments/
|
|
25
|
+
!experiments/banded_ridge_lmer_mvp.py
|
|
26
|
+
ideas/
|
|
27
|
+
bambi/
|
|
28
|
+
burntends/tests/*.csv
|
|
29
|
+
lmer-refactor-notes/
|
|
30
|
+
scripts/
|
|
31
|
+
*.txt
|
|
32
|
+
*.json
|
|
33
|
+
!tests/pyodide/package.json
|
|
34
|
+
*.png
|
|
35
|
+
|
|
36
|
+
# Node.js (for Pyodide tests)
|
|
37
|
+
node_modules/
|
|
38
|
+
|
|
39
|
+
# Parity testing traces
|
|
40
|
+
tests/parity/traces/
|
|
41
|
+
|
|
42
|
+
# Coverage reports
|
|
43
|
+
coverage_reports/
|
|
44
|
+
.coverage
|
|
45
|
+
htmlcov/
|
|
46
|
+
# pixi environments
|
|
47
|
+
.pixi/*
|
|
48
|
+
!.pixi/config.toml
|
|
49
|
+
tests/**/*.csv
|
|
50
|
+
bossanova-docs/_site/
|
|
51
|
+
bossanova-docs/_freeze/
|
|
52
|
+
bossanova-docs/.jupyter_cache/
|
|
53
|
+
bossanova-docs/**/.jupyter_cache/
|
|
54
|
+
claude-logs/
|
|
55
|
+
.beads/
|
|
56
|
+
benchmarking
|
|
57
|
+
|
|
58
|
+
# Build artifacts
|
|
59
|
+
build/
|
|
60
|
+
dist/
|
|
61
|
+
|
|
62
|
+
# Logs
|
|
63
|
+
*.log
|
|
64
|
+
bossanova-docs/reference/
|
|
65
|
+
|
|
66
|
+
# Refs
|
|
67
|
+
MixedModels.jl/
|
|
68
|
+
lme4/
|
|
69
|
+
.env*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Eshin Jolly, SciMinds Research Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bossanova
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Bridging statistical cultures with some jazz
|
|
5
|
+
Author: Eshin Jolly
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: analysis,multi-level-modeling,regression,statistics
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: jax>=0.8.0
|
|
14
|
+
Requires-Dist: joblib>=1.3.0
|
|
15
|
+
Requires-Dist: nlopt>=2.9.0
|
|
16
|
+
Requires-Dist: numpy>=2.3.4
|
|
17
|
+
Requires-Dist: pandas>=2.0.0
|
|
18
|
+
Requires-Dist: polars>=1.0.0
|
|
19
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
20
|
+
Requires-Dist: scikit-sparse>=0.4.16
|
|
21
|
+
Requires-Dist: scipy>=1.14.0
|
|
22
|
+
Requires-Dist: tqdm>=4.66.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Bossanova
|
|
26
|
+
|
|
27
|
+
Bridging statistical cultures with some jazz.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""bossanova - Clean Python implementation of R's formula-based statistical models.
|
|
2
|
+
|
|
3
|
+
A modern Python library providing formula-based model fitting for:
|
|
4
|
+
|
|
5
|
+
- lm: Linear models (OLS regression)
|
|
6
|
+
- glm: Generalized linear models (logistic, Poisson, etc.)
|
|
7
|
+
- lmer: Linear mixed-effects models
|
|
8
|
+
- glmer: Generalized linear mixed-effects models
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
>>> from bossanova import lm
|
|
12
|
+
>>> model = lm("mpg ~ wt + hp", data=mtcars)
|
|
13
|
+
>>> model.fit()
|
|
14
|
+
>>> model.summary()
|
|
15
|
+
|
|
16
|
+
Backend Selection:
|
|
17
|
+
By default, bossanova uses JAX for optimal performance. You can switch
|
|
18
|
+
to NumPy before fitting any models:
|
|
19
|
+
|
|
20
|
+
>>> import bossanova
|
|
21
|
+
>>> bossanova.set_backend("numpy")
|
|
22
|
+
>>> model = bossanova.lm("y ~ x", data=df).fit()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Backend API (must be imported first, before any JAX usage)
|
|
26
|
+
from bossanova._backend import backend, get_backend, set_backend
|
|
27
|
+
|
|
28
|
+
# Configure JAX x64 eagerly if JAX is available
|
|
29
|
+
# This MUST happen before any JAX arrays are created anywhere in the codebase.
|
|
30
|
+
# We cannot defer this to lazy loading because submodules (e.g., lmer_core.py)
|
|
31
|
+
# import JAX at module level and would create float32 arrays otherwise.
|
|
32
|
+
try:
|
|
33
|
+
import jax
|
|
34
|
+
|
|
35
|
+
jax.config.update("jax_enable_x64", True)
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass # JAX not available, will use numpy backend
|
|
38
|
+
|
|
39
|
+
# Configuration
|
|
40
|
+
from bossanova._config import ( # noqa: E402
|
|
41
|
+
get_singular_tolerance,
|
|
42
|
+
set_singular_tolerance,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Data loading
|
|
46
|
+
from bossanova.data import ( # noqa: E402
|
|
47
|
+
load_dataset,
|
|
48
|
+
show_datasets,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Models
|
|
52
|
+
from bossanova.models import glm, glmer, lm, lmer, ridge # noqa: E402
|
|
53
|
+
|
|
54
|
+
# Statistics
|
|
55
|
+
from bossanova.stats import compare, lrt # noqa: E402
|
|
56
|
+
|
|
57
|
+
# Visualization
|
|
58
|
+
from bossanova import viz # noqa: E402
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Backend
|
|
62
|
+
"get_backend",
|
|
63
|
+
"set_backend",
|
|
64
|
+
"backend",
|
|
65
|
+
# Models
|
|
66
|
+
"lm",
|
|
67
|
+
"glm",
|
|
68
|
+
"lmer",
|
|
69
|
+
"glmer",
|
|
70
|
+
"ridge",
|
|
71
|
+
# Model comparison
|
|
72
|
+
"compare",
|
|
73
|
+
"lrt",
|
|
74
|
+
# Data loading
|
|
75
|
+
"load_dataset",
|
|
76
|
+
"show_datasets",
|
|
77
|
+
# Configuration
|
|
78
|
+
"get_singular_tolerance",
|
|
79
|
+
"set_singular_tolerance",
|
|
80
|
+
# Visualization
|
|
81
|
+
"viz",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
from importlib.metadata import version as _get_version
|
|
85
|
+
|
|
86
|
+
__version__ = _get_version("bossanova")
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Backend detection and switching for bossanova.
|
|
2
|
+
|
|
3
|
+
This module provides the infrastructure for switching between JAX and NumPy
|
|
4
|
+
backends at runtime. The backend is auto-detected on first use but can be
|
|
5
|
+
explicitly set before any model fitting occurs.
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
>>> import bossanova
|
|
9
|
+
>>> bossanova.get_backend()
|
|
10
|
+
'jax'
|
|
11
|
+
>>> bossanova.set_backend("numpy")
|
|
12
|
+
>>> bossanova.get_backend()
|
|
13
|
+
'numpy'
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
BackendName = Literal["jax", "numpy"]
|
|
21
|
+
|
|
22
|
+
# Global state
|
|
23
|
+
_backend: BackendName | None = None
|
|
24
|
+
_backend_locked: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _detect_backend() -> BackendName:
|
|
28
|
+
"""Auto-detect the best available backend.
|
|
29
|
+
|
|
30
|
+
Detection order:
|
|
31
|
+
1. If running in Pyodide (emscripten), use numpy (JAX not available)
|
|
32
|
+
2. Try to import JAX - if successful, use jax
|
|
33
|
+
3. Fall back to numpy
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The detected backend name.
|
|
37
|
+
"""
|
|
38
|
+
# Pyodide detection
|
|
39
|
+
if sys.platform == "emscripten":
|
|
40
|
+
return "numpy"
|
|
41
|
+
|
|
42
|
+
# Try JAX
|
|
43
|
+
try:
|
|
44
|
+
import jax
|
|
45
|
+
|
|
46
|
+
# Enable float64 precision - must be done before any array creation
|
|
47
|
+
jax.config.update("jax_enable_x64", True)
|
|
48
|
+
return "jax"
|
|
49
|
+
except ImportError:
|
|
50
|
+
return "numpy"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_backend() -> BackendName:
|
|
54
|
+
"""Get the current backend name.
|
|
55
|
+
|
|
56
|
+
If no backend has been explicitly set, auto-detects the best available
|
|
57
|
+
backend on first call.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The current backend name ('jax' or 'numpy').
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
>>> import bossanova
|
|
64
|
+
>>> bossanova.get_backend()
|
|
65
|
+
'jax'
|
|
66
|
+
"""
|
|
67
|
+
global _backend
|
|
68
|
+
if _backend is None:
|
|
69
|
+
_backend = _detect_backend()
|
|
70
|
+
return _backend
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_backend(name: BackendName) -> None:
|
|
74
|
+
"""Set the backend to use for computations.
|
|
75
|
+
|
|
76
|
+
Must be called before any model fitting occurs. Once a model has been
|
|
77
|
+
fitted, the backend is locked and cannot be changed.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: Backend name, either 'jax' or 'numpy'.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RuntimeError: If called after a model has been fitted.
|
|
84
|
+
ValueError: If name is not 'jax' or 'numpy'.
|
|
85
|
+
ImportError: If 'jax' is requested but JAX is not installed.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> import bossanova
|
|
89
|
+
>>> bossanova.set_backend("numpy")
|
|
90
|
+
>>> bossanova.get_backend()
|
|
91
|
+
'numpy'
|
|
92
|
+
"""
|
|
93
|
+
global _backend, _backend_locked
|
|
94
|
+
if _backend_locked:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"Cannot change backend after models have been fitted. "
|
|
97
|
+
"Call set_backend() before any model fitting."
|
|
98
|
+
)
|
|
99
|
+
if name not in ("jax", "numpy"):
|
|
100
|
+
raise ValueError(f"Unknown backend: {name}. Use 'jax' or 'numpy'.")
|
|
101
|
+
|
|
102
|
+
# Validate JAX availability early if requested
|
|
103
|
+
if name == "jax":
|
|
104
|
+
try:
|
|
105
|
+
import jax
|
|
106
|
+
|
|
107
|
+
jax.config.update("jax_enable_x64", True)
|
|
108
|
+
except ImportError as e:
|
|
109
|
+
raise ImportError(
|
|
110
|
+
"JAX is not installed. Install it with 'pip install jax jaxlib' "
|
|
111
|
+
"or use set_backend('numpy')."
|
|
112
|
+
) from e
|
|
113
|
+
|
|
114
|
+
_backend = name
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _lock_backend() -> None:
|
|
118
|
+
"""Lock the backend to prevent switching after model fitting.
|
|
119
|
+
|
|
120
|
+
This is called internally when models are fitted to ensure consistent
|
|
121
|
+
behavior throughout a session.
|
|
122
|
+
"""
|
|
123
|
+
global _backend, _backend_locked
|
|
124
|
+
# Ensure backend is initialized before locking
|
|
125
|
+
if _backend is None:
|
|
126
|
+
_backend = _detect_backend()
|
|
127
|
+
_backend_locked = True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _is_backend_locked() -> bool:
|
|
131
|
+
"""Check if the backend is locked.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if backend is locked, False otherwise.
|
|
135
|
+
"""
|
|
136
|
+
return _backend_locked
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _reset_backend() -> None:
|
|
140
|
+
"""Reset backend state (for testing only).
|
|
141
|
+
|
|
142
|
+
Warning:
|
|
143
|
+
This should only be used in tests. Using it in production code
|
|
144
|
+
can lead to inconsistent behavior.
|
|
145
|
+
"""
|
|
146
|
+
global _backend, _backend_locked
|
|
147
|
+
_backend = None
|
|
148
|
+
_backend_locked = False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@contextmanager
|
|
152
|
+
def backend(name: BackendName):
|
|
153
|
+
"""Context manager for temporary backend switching.
|
|
154
|
+
|
|
155
|
+
This is primarily intended for testing. It temporarily switches the
|
|
156
|
+
backend and restores the previous state on exit.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
name: Backend name to use within the context.
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
None
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> import bossanova
|
|
166
|
+
>>> with bossanova.backend("numpy"):
|
|
167
|
+
... print(bossanova.get_backend())
|
|
168
|
+
'numpy'
|
|
169
|
+
"""
|
|
170
|
+
global _backend, _backend_locked
|
|
171
|
+
old_backend = _backend
|
|
172
|
+
old_locked = _backend_locked
|
|
173
|
+
|
|
174
|
+
# Temporarily switch
|
|
175
|
+
_backend = name
|
|
176
|
+
_backend_locked = False
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
yield
|
|
180
|
+
finally:
|
|
181
|
+
# Restore previous state
|
|
182
|
+
_backend = old_backend
|
|
183
|
+
_backend_locked = old_locked
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Global configuration for bossanova.
|
|
2
|
+
|
|
3
|
+
This module provides package-wide configuration settings that can be modified
|
|
4
|
+
at runtime. Currently manages singular tolerance for mixed models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Default singular tolerance matches lme4's default (utilities.R:924-928)
|
|
8
|
+
_SINGULAR_TOLERANCE: float = 1e-4
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_singular_tolerance() -> float:
|
|
12
|
+
"""Get the current singular tolerance for mixed models.
|
|
13
|
+
|
|
14
|
+
The singular tolerance is used by `isSingular()` to determine if a mixed
|
|
15
|
+
model fit is singular (has variance components at or near zero).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The current singular tolerance threshold.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
>>> from bossanova import get_singular_tolerance
|
|
22
|
+
>>> get_singular_tolerance()
|
|
23
|
+
0.0001
|
|
24
|
+
"""
|
|
25
|
+
return _SINGULAR_TOLERANCE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_singular_tolerance(tol: float) -> None:
|
|
29
|
+
"""Set the global singular tolerance for mixed models.
|
|
30
|
+
|
|
31
|
+
The singular tolerance is used by `isSingular()` to determine if a mixed
|
|
32
|
+
model fit is singular. Values below this threshold are considered
|
|
33
|
+
effectively zero.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
tol: New tolerance threshold. Must be positive.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If tol is not positive.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> from bossanova import set_singular_tolerance
|
|
43
|
+
>>> set_singular_tolerance(1e-6) # More strict threshold
|
|
44
|
+
"""
|
|
45
|
+
global _SINGULAR_TOLERANCE
|
|
46
|
+
if tol <= 0:
|
|
47
|
+
raise ValueError(f"Tolerance must be positive, got {tol}")
|
|
48
|
+
_SINGULAR_TOLERANCE = tol
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Formula parsing infrastructure.
|
|
2
|
+
|
|
3
|
+
This module provides a recursive descent parser for statistical formula strings
|
|
4
|
+
(e.g., "y ~ x1 + x2 * group"). It is vendored from the formulae library.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
Scanner: Tokenize formula strings into Token objects.
|
|
8
|
+
Parser: Parse Token sequences into AST nodes.
|
|
9
|
+
ScanError: Raised when scanning fails.
|
|
10
|
+
ParseError: Raised when parsing fails.
|
|
11
|
+
|
|
12
|
+
AST Node Types:
|
|
13
|
+
Variable: Variable reference (e.g., "x")
|
|
14
|
+
Literal: Literal value (numbers, strings)
|
|
15
|
+
Binary: Binary operation (e.g., "x + y", "x ~ y")
|
|
16
|
+
Unary: Unary operation (e.g., "-x")
|
|
17
|
+
Call: Function call (e.g., "C(x)", "center(y)")
|
|
18
|
+
Grouping: Parenthesized expression
|
|
19
|
+
QuotedName: Back-quoted name (e.g., "`weird name`")
|
|
20
|
+
Assign: Assignment expression (e.g., "reference='A'")
|
|
21
|
+
Token: Single token from scanner.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> from bossanova._parser import Scanner, Parser
|
|
25
|
+
>>> tokens = Scanner("y ~ x1 + x2").scan()
|
|
26
|
+
>>> ast = Parser(tokens).parse()
|
|
27
|
+
>>> ast
|
|
28
|
+
Binary(left=Literal(value=1), op='~', right=...)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .expr import Assign, Binary, Call, Grouping, Literal, QuotedName, Unary, Variable
|
|
32
|
+
from .parser import ParseError, Parser
|
|
33
|
+
from .scanner import ScanError, Scanner
|
|
34
|
+
from .token import Token
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Scanner/Parser
|
|
38
|
+
"Scanner",
|
|
39
|
+
"Parser",
|
|
40
|
+
"ScanError",
|
|
41
|
+
"ParseError",
|
|
42
|
+
# Token
|
|
43
|
+
"Token",
|
|
44
|
+
# AST Nodes
|
|
45
|
+
"Assign",
|
|
46
|
+
"Binary",
|
|
47
|
+
"Call",
|
|
48
|
+
"Grouping",
|
|
49
|
+
"Literal",
|
|
50
|
+
"QuotedName",
|
|
51
|
+
"Unary",
|
|
52
|
+
"Variable",
|
|
53
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""AST expression node types for formula parsing.
|
|
2
|
+
|
|
3
|
+
Vendored from formulae library (https://github.com/bambinos/formulae).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .token import Token
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Assign:
|
|
15
|
+
"""Expression for assignments (e.g., x=value in function calls)."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: Variable, value: object) -> None:
|
|
18
|
+
self.name = name
|
|
19
|
+
self.value = value
|
|
20
|
+
|
|
21
|
+
def __eq__(self, other: object) -> bool:
|
|
22
|
+
if not isinstance(other, Assign):
|
|
23
|
+
return NotImplemented
|
|
24
|
+
return self.name == other.name and self.value == other.value
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"Assign(name={self.name}, value={self.value})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Grouping:
|
|
31
|
+
"""Expression for parenthesized groups."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, expression: object) -> None:
|
|
34
|
+
self.expression = expression
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: object) -> bool:
|
|
37
|
+
if not isinstance(other, Grouping):
|
|
38
|
+
return NotImplemented
|
|
39
|
+
return self.expression == other.expression
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return f"Grouping({self.expression})"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Binary:
|
|
46
|
+
"""Expression for binary operations (e.g., x + y, x ~ y)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, left: object, operator: Token, right: object) -> None:
|
|
49
|
+
self.left = left
|
|
50
|
+
self.operator = operator
|
|
51
|
+
self.right = right
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other: object) -> bool:
|
|
54
|
+
if not isinstance(other, Binary):
|
|
55
|
+
return NotImplemented
|
|
56
|
+
return (
|
|
57
|
+
self.left == other.left
|
|
58
|
+
and self.operator == other.operator
|
|
59
|
+
and self.right == other.right
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
return (
|
|
64
|
+
f"Binary(left={self.left}, op={self.operator.lexeme!r}, right={self.right})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Unary:
|
|
69
|
+
"""Expression for unary operations (e.g., -x, +x)."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, operator: Token, right: object) -> None:
|
|
72
|
+
self.operator = operator
|
|
73
|
+
self.right = right
|
|
74
|
+
|
|
75
|
+
def __eq__(self, other: object) -> bool:
|
|
76
|
+
if not isinstance(other, Unary):
|
|
77
|
+
return NotImplemented
|
|
78
|
+
return self.operator == other.operator and self.right == other.right
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
return f"Unary(op={self.operator.lexeme!r}, right={self.right})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Call:
|
|
85
|
+
"""Expression for function calls (e.g., C(x), center(y))."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, callee: object, args: list) -> None:
|
|
88
|
+
self.callee = callee
|
|
89
|
+
self.args = args
|
|
90
|
+
|
|
91
|
+
def __eq__(self, other: object) -> bool:
|
|
92
|
+
if not isinstance(other, Call):
|
|
93
|
+
return NotImplemented
|
|
94
|
+
return self.callee == other.callee and self.args == other.args
|
|
95
|
+
|
|
96
|
+
def __repr__(self) -> str:
|
|
97
|
+
return f"Call(callee={self.callee}, args={self.args})"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Variable:
|
|
101
|
+
"""Expression for variable references."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, name: Token, level: Literal | None = None) -> None:
|
|
104
|
+
self.name = name
|
|
105
|
+
self.level = level
|
|
106
|
+
|
|
107
|
+
def __eq__(self, other: object) -> bool:
|
|
108
|
+
if not isinstance(other, Variable):
|
|
109
|
+
return NotImplemented
|
|
110
|
+
return self.name == other.name and self.level == other.level
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str:
|
|
113
|
+
if self.level is not None:
|
|
114
|
+
return f"Variable(name={self.name.lexeme!r}, level={self.level.value!r})"
|
|
115
|
+
return f"Variable(name={self.name.lexeme!r})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class QuotedName:
|
|
119
|
+
"""Expression for back-quoted names (e.g., `weird column name!`)."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, expression: Token) -> None:
|
|
122
|
+
self.expression = expression
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other: object) -> bool:
|
|
125
|
+
if not isinstance(other, QuotedName):
|
|
126
|
+
return NotImplemented
|
|
127
|
+
return self.expression == other.expression
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return f"QuotedName({self.expression.lexeme!r})"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Literal:
|
|
134
|
+
"""Expression for literal values (numbers, strings, etc.)."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, value: object, lexeme: str | None = None) -> None:
|
|
137
|
+
self.value = value
|
|
138
|
+
self.lexeme = lexeme
|
|
139
|
+
|
|
140
|
+
def __eq__(self, other: object) -> bool:
|
|
141
|
+
if not isinstance(other, Literal):
|
|
142
|
+
return NotImplemented
|
|
143
|
+
return self.value == other.value and self.lexeme == other.lexeme
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
if self.lexeme is not None:
|
|
147
|
+
return f"Literal(value={self.value!r}, lexeme={self.lexeme!r})"
|
|
148
|
+
return f"Literal(value={self.value!r})"
|