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.
Files changed (136) hide show
  1. bossanova-0.1.0.dev0/.gitignore +69 -0
  2. bossanova-0.1.0.dev0/LICENSE +21 -0
  3. bossanova-0.1.0.dev0/PKG-INFO +27 -0
  4. bossanova-0.1.0.dev0/README.md +3 -0
  5. bossanova-0.1.0.dev0/bossanova/__init__.py +86 -0
  6. bossanova-0.1.0.dev0/bossanova/_backend.py +183 -0
  7. bossanova-0.1.0.dev0/bossanova/_config.py +48 -0
  8. bossanova-0.1.0.dev0/bossanova/_parser/__init__.py +53 -0
  9. bossanova-0.1.0.dev0/bossanova/_parser/expr.py +148 -0
  10. bossanova-0.1.0.dev0/bossanova/_parser/parser.py +260 -0
  11. bossanova-0.1.0.dev0/bossanova/_parser/scanner.py +240 -0
  12. bossanova-0.1.0.dev0/bossanova/_parser/token.py +30 -0
  13. bossanova-0.1.0.dev0/bossanova/_utils.py +37 -0
  14. bossanova-0.1.0.dev0/bossanova/data/__init__.py +115 -0
  15. bossanova-0.1.0.dev0/bossanova/data/advertising.csv +201 -0
  16. bossanova-0.1.0.dev0/bossanova/data/chickweight.csv +579 -0
  17. bossanova-0.1.0.dev0/bossanova/data/credit.csv +401 -0
  18. bossanova-0.1.0.dev0/bossanova/data/gammas.csv +6001 -0
  19. bossanova-0.1.0.dev0/bossanova/data/mtcars.csv +33 -0
  20. bossanova-0.1.0.dev0/bossanova/data/penguins.csv +345 -0
  21. bossanova-0.1.0.dev0/bossanova/data/poker.csv +301 -0
  22. bossanova-0.1.0.dev0/bossanova/data/sleep.csv +181 -0
  23. bossanova-0.1.0.dev0/bossanova/data/titanic.csv +892 -0
  24. bossanova-0.1.0.dev0/bossanova/data/titanic_test.csv +419 -0
  25. bossanova-0.1.0.dev0/bossanova/data/titanic_train.csv +892 -0
  26. bossanova-0.1.0.dev0/bossanova/formula/__init__.py +94 -0
  27. bossanova-0.1.0.dev0/bossanova/formula/contrasts.py +555 -0
  28. bossanova-0.1.0.dev0/bossanova/formula/design.py +1976 -0
  29. bossanova-0.1.0.dev0/bossanova/formula/encoding.py +298 -0
  30. bossanova-0.1.0.dev0/bossanova/formula/random_effects.py +131 -0
  31. bossanova-0.1.0.dev0/bossanova/formula/transforms.py +577 -0
  32. bossanova-0.1.0.dev0/bossanova/formula/z_matrix.py +538 -0
  33. bossanova-0.1.0.dev0/bossanova/grammar/__init__.py +204 -0
  34. bossanova-0.1.0.dev0/bossanova/grammar/assuming.py +227 -0
  35. bossanova-0.1.0.dev0/bossanova/grammar/explain.py +610 -0
  36. bossanova-0.1.0.dev0/bossanova/grammar/explore.py +682 -0
  37. bossanova-0.1.0.dev0/bossanova/grammar/fit.py +142 -0
  38. bossanova-0.1.0.dev0/bossanova/grammar/infer.py +812 -0
  39. bossanova-0.1.0.dev0/bossanova/grammar/model.py +269 -0
  40. bossanova-0.1.0.dev0/bossanova/grammar/pipe.py +133 -0
  41. bossanova-0.1.0.dev0/bossanova/grammar/predict.py +672 -0
  42. bossanova-0.1.0.dev0/bossanova/grammar/results.py +2354 -0
  43. bossanova-0.1.0.dev0/bossanova/grammar/specs.py +984 -0
  44. bossanova-0.1.0.dev0/bossanova/grammar/strategies.py +1802 -0
  45. bossanova-0.1.0.dev0/bossanova/grammar/viz.py +162 -0
  46. bossanova-0.1.0.dev0/bossanova/marginal/__init__.py +81 -0
  47. bossanova-0.1.0.dev0/bossanova/marginal/contrasts.py +225 -0
  48. bossanova-0.1.0.dev0/bossanova/marginal/emm.py +428 -0
  49. bossanova-0.1.0.dev0/bossanova/marginal/grid.py +216 -0
  50. bossanova-0.1.0.dev0/bossanova/marginal/hypothesis.py +313 -0
  51. bossanova-0.1.0.dev0/bossanova/marginal/joint_tests.py +356 -0
  52. bossanova-0.1.0.dev0/bossanova/marginal/parser.py +275 -0
  53. bossanova-0.1.0.dev0/bossanova/marginal/slopes.py +234 -0
  54. bossanova-0.1.0.dev0/bossanova/models/__init__.py +16 -0
  55. bossanova-0.1.0.dev0/bossanova/models/base/__init__.py +6 -0
  56. bossanova-0.1.0.dev0/bossanova/models/base/mixed.py +1527 -0
  57. bossanova-0.1.0.dev0/bossanova/models/base/model.py +2905 -0
  58. bossanova-0.1.0.dev0/bossanova/models/display.py +189 -0
  59. bossanova-0.1.0.dev0/bossanova/models/glm.py +1051 -0
  60. bossanova-0.1.0.dev0/bossanova/models/glmer.py +1430 -0
  61. bossanova-0.1.0.dev0/bossanova/models/lm.py +818 -0
  62. bossanova-0.1.0.dev0/bossanova/models/lmer.py +1511 -0
  63. bossanova-0.1.0.dev0/bossanova/models/ridge.py +1139 -0
  64. bossanova-0.1.0.dev0/bossanova/ops/__init__.py +77 -0
  65. bossanova-0.1.0.dev0/bossanova/ops/_array_ops.py +259 -0
  66. bossanova-0.1.0.dev0/bossanova/ops/_get_ops.py +60 -0
  67. bossanova-0.1.0.dev0/bossanova/ops/_jax_backend.py +169 -0
  68. bossanova-0.1.0.dev0/bossanova/ops/_numpy_backend.py +169 -0
  69. bossanova-0.1.0.dev0/bossanova/ops/batching.py +129 -0
  70. bossanova-0.1.0.dev0/bossanova/ops/causal/__init__.py +48 -0
  71. bossanova-0.1.0.dev0/bossanova/ops/causal/dag_viz.py +526 -0
  72. bossanova-0.1.0.dev0/bossanova/ops/causal/graphs.py +386 -0
  73. bossanova-0.1.0.dev0/bossanova/ops/causal/identify.py +350 -0
  74. bossanova-0.1.0.dev0/bossanova/ops/causal/parse.py +227 -0
  75. bossanova-0.1.0.dev0/bossanova/ops/causal/patterns.py +362 -0
  76. bossanova-0.1.0.dev0/bossanova/ops/convergence.py +393 -0
  77. bossanova-0.1.0.dev0/bossanova/ops/diagnostics.py +241 -0
  78. bossanova-0.1.0.dev0/bossanova/ops/family.py +726 -0
  79. bossanova-0.1.0.dev0/bossanova/ops/glm_fit.py +508 -0
  80. bossanova-0.1.0.dev0/bossanova/ops/glmer_pirls.py +1350 -0
  81. bossanova-0.1.0.dev0/bossanova/ops/inference.py +333 -0
  82. bossanova-0.1.0.dev0/bossanova/ops/initialization.py +356 -0
  83. bossanova-0.1.0.dev0/bossanova/ops/lambda_builder.py +800 -0
  84. bossanova-0.1.0.dev0/bossanova/ops/linalg.py +374 -0
  85. bossanova-0.1.0.dev0/bossanova/ops/lmer_core.py +613 -0
  86. bossanova-0.1.0.dev0/bossanova/ops/predict.py +76 -0
  87. bossanova-0.1.0.dev0/bossanova/ops/ridge_fit.py +384 -0
  88. bossanova-0.1.0.dev0/bossanova/ops/ridge_inference.py +48 -0
  89. bossanova-0.1.0.dev0/bossanova/ops/rng.py +321 -0
  90. bossanova-0.1.0.dev0/bossanova/ops/sparse_solver.py +376 -0
  91. bossanova-0.1.0.dev0/bossanova/optimize/__init__.py +5 -0
  92. bossanova-0.1.0.dev0/bossanova/optimize/bobyqa.py +171 -0
  93. bossanova-0.1.0.dev0/bossanova/py.typed +0 -0
  94. bossanova-0.1.0.dev0/bossanova/resample/__init__.py +120 -0
  95. bossanova-0.1.0.dev0/bossanova/resample/core.py +412 -0
  96. bossanova-0.1.0.dev0/bossanova/resample/glm.py +810 -0
  97. bossanova-0.1.0.dev0/bossanova/resample/lm.py +1171 -0
  98. bossanova-0.1.0.dev0/bossanova/resample/mixed.py +976 -0
  99. bossanova-0.1.0.dev0/bossanova/resample/results.py +294 -0
  100. bossanova-0.1.0.dev0/bossanova/resample/ridge.py +476 -0
  101. bossanova-0.1.0.dev0/bossanova/resample/utils.py +183 -0
  102. bossanova-0.1.0.dev0/bossanova/results/__init__.py +84 -0
  103. bossanova-0.1.0.dev0/bossanova/results/builders.py +1155 -0
  104. bossanova-0.1.0.dev0/bossanova/results/schemas.py +675 -0
  105. bossanova-0.1.0.dev0/bossanova/simulation/__init__.py +60 -0
  106. bossanova-0.1.0.dev0/bossanova/simulation/dgp/__init__.py +17 -0
  107. bossanova-0.1.0.dev0/bossanova/simulation/dgp/glm.py +180 -0
  108. bossanova-0.1.0.dev0/bossanova/simulation/dgp/glmer.py +183 -0
  109. bossanova-0.1.0.dev0/bossanova/simulation/dgp/lm.py +108 -0
  110. bossanova-0.1.0.dev0/bossanova/simulation/dgp/lmer.py +184 -0
  111. bossanova-0.1.0.dev0/bossanova/simulation/harness.py +341 -0
  112. bossanova-0.1.0.dev0/bossanova/simulation/metrics.py +149 -0
  113. bossanova-0.1.0.dev0/bossanova/stats/__init__.py +31 -0
  114. bossanova-0.1.0.dev0/bossanova/stats/compare.py +1019 -0
  115. bossanova-0.1.0.dev0/bossanova/stats/effect_sizes.py +180 -0
  116. bossanova-0.1.0.dev0/bossanova/stats/lrt.py +69 -0
  117. bossanova-0.1.0.dev0/bossanova/stats/satterthwaite.py +945 -0
  118. bossanova-0.1.0.dev0/bossanova/viz/__init__.py +80 -0
  119. bossanova-0.1.0.dev0/bossanova/viz/_core.py +493 -0
  120. bossanova-0.1.0.dev0/bossanova/viz/cognition.py +250 -0
  121. bossanova-0.1.0.dev0/bossanova/viz/compare.py +299 -0
  122. bossanova-0.1.0.dev0/bossanova/viz/dag.py +416 -0
  123. bossanova-0.1.0.dev0/bossanova/viz/design.py +414 -0
  124. bossanova-0.1.0.dev0/bossanova/viz/fit.py +313 -0
  125. bossanova-0.1.0.dev0/bossanova/viz/lattice.py +384 -0
  126. bossanova-0.1.0.dev0/bossanova/viz/layout.py +562 -0
  127. bossanova-0.1.0.dev0/bossanova/viz/mem.py +426 -0
  128. bossanova-0.1.0.dev0/bossanova/viz/params.py +233 -0
  129. bossanova-0.1.0.dev0/bossanova/viz/predict.py +528 -0
  130. bossanova-0.1.0.dev0/bossanova/viz/ranef.py +300 -0
  131. bossanova-0.1.0.dev0/bossanova/viz/relationships.py +209 -0
  132. bossanova-0.1.0.dev0/bossanova/viz/resid.py +402 -0
  133. bossanova-0.1.0.dev0/bossanova/viz/vif.py +302 -0
  134. bossanova-0.1.0.dev0/pyproject.toml +186 -0
  135. bossanova-0.1.0.dev0/tests/bossanova_benchmarks/bootstrap/data/README.md +31 -0
  136. 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,3 @@
1
+ # Bossanova
2
+
3
+ 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})"