otko/docs/adr/ADR-0002-headless-gui-dep-split.md

91 lines
3.5 KiB
Markdown
Raw Permalink Normal View History

2026-09-08 02:12:15 -04:00
# ADR-0002 — Split pyproject dependencies into headless base and `gui` optional extra
| Field | Value |
|---|---|
| **Status** | Accepted |
| **Date** | 2026-06-15 |
| **Author** | ogunc |
---
## 1. Context
`otko.core` is pure Pydantic v2 (no Qt, no OpenSeesPy imports — stated
explicitly in `core/__init__.py`). `otko.services` adds NumPy, h5py, and
OpenSeesPy for headless computation. Together, these two packages can be used from
scripts, Jupyter notebooks, and web backends **without any Qt or 3D-rendering stack**.
Before this ADR, every `pip install otko` pulled in PySide6, pyvista,
pyvistaqt, vtk, pyqtgraph, and imageio — roughly 800 MB of GUI/visualization
packages — even when only the headless computation layer was needed. Web backends and
CI machines without a display had to work around this with `--no-deps`, which is
fragile and skips genuine compute-layer deps (numpy, h5py) too.
## 2. Decision
Split `[project.dependencies]` into two tiers in `pyproject.toml`:
### Headless base (`pip install -e .`)
Packages imported by `core/` and the non-Qt parts of `services/`:
| Package | Where used |
|---------|-----------|
| `pydantic>=2.5` | All `core/` modules, `material_tester.py` |
| `numpy>=1.26` | `services/` computation modules (7 files) |
| `h5py>=3.10` | `opensees_runner._run_transient()`, `TransientResults` accessors |
| `openseespy==3.8.0.0` | Lazy import in `OpenSeesRunner.__init__` |
| `openseespywin==3.8.0.0 ; sys_platform=='win32'` | Windows DLL companion |
### GUI extra (`pip install -e ".[gui]"`)
Packages only needed by `views/`, `viewmodels/`, `commands/`, `qt_workers.py`,
and `animation_export.py` (which drives a live PyVista plotter):
`PySide6`, `pyvista`, `pyvistaqt`, `vtk`, `pyqtgraph`, `imageio[ffmpeg]`,
`scipy` (forward-compat, currently a phantom dep), `pandas` (same).
## 3. Consequences
- **Desktop developers** install with `pip install -e ".[gui,dev]"`. No change to
what gets installed; only the install command changes from `.[dev]` → `.[gui,dev]`.
- **Web backends / scripts / notebooks** install with `pip install -e .` (or
`pip install otko`) and get a lean ~50 MB environment.
- **otko-web** can drop the `--no-deps` workaround and install the
package normally. The web backend's `requirements.txt` no longer needs to list
pydantic/numpy/h5py separately — they come from the base install.
- **scipy and pandas** are listed under `[gui]` as phantom deps (currently never
imported anywhere in the codebase). They are kept to avoid surprise breakage if a
future feature adds them; audited and flagged on 2026-06-15.
## 4. Verification
After installing only the base set:
```python
import sys
from otko.core import Project
from otko.services.opensees_runner import OpenSeesRunner
from otko.services.material_tester import test_uniaxial_material
# Run a modal analysis on the bundled two-storey shear frame example
import json
from pathlib import Path
from otko.core import ModalCase
data = json.loads(Path("examples/eigen_two_storey_shear_frame.osmodel").read_text())
project = Project.model_validate(data)
modal_case = next(c for c in project.analyses if isinstance(c, ModalCase))
runner = OpenSeesRunner(project)
runner.build()
results = runner._run_modal(modal_case)
assert len(results.eigenvalues) == 2
assert all(ev > 0 for ev in results.eigenvalues)
gui_packages = {"PySide6", "pyvista", "pyvistaqt", "vtk", "pyqtgraph"}
assert not gui_packages.intersection(sys.modules), \
f"GUI package imported: {gui_packages & sys.modules.keys()}"
print("PASS — headless modal analysis complete, no GUI packages imported")
```