diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..5a778fe --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# SessionStart hook for libdedx. +# +# Prepares a fresh Claude Code on the web container so that builds, tests, +# linters and the Python bindings work immediately in-session: +# - configures + builds the debug preset (produces compile_commands.json for +# clang-tidy and build/src/libdedx.so for the Python ctypes binding) +# - installs the Python package in editable mode with dev dependencies +# - exports LIBDEDX_SO so `pytest` under python/ can find the shared library +# +# Safe to run multiple times. Only runs in the remote (web) environment. +set -euo pipefail + +# Only set up the toolchain in Claude Code on the web; local checkouts are +# assumed to already have a working dev environment. +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" + +echo "[session-start] Configuring debug preset..." +cmake --preset debug + +echo "[session-start] Building debug preset..." +cmake --build --preset debug --parallel + +# Editable install of the ctypes binding plus dev tools (ruff, pytest). +# Ubuntu's system Python is externally managed (PEP 668); fall back to +# --break-system-packages when a normal editable install is refused. +echo "[session-start] Installing Python package (editable, with dev deps)..." +python3 -m pip install -e "python[dev]" \ + || python3 -m pip install --break-system-packages -e "python[dev]" + +# Point the Python test suite at the freshly built shared library so that +# `pytest` works without any manual setup. +LIBDEDX_SO="${CLAUDE_PROJECT_DIR:-$(pwd)}/build/src/libdedx.so" +export LIBDEDX_SO +# Idempotent: only append once, so repeated hook runs don't clutter the env file. +if [ -n "${CLAUDE_ENV_FILE:-}" ] \ + && ! grep -qs '^export LIBDEDX_SO=' "$CLAUDE_ENV_FILE"; then + echo "export LIBDEDX_SO=\"${LIBDEDX_SO}\"" >> "$CLAUDE_ENV_FILE" +fi + +echo "[session-start] Done. Build, tests (ctest), clang-tidy and pytest are ready." diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f39a2af --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,36 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + }, + "permissions": { + "allow": [ + "Bash(cmake --preset:*)", + "Bash(cmake --build:*)", + "Bash(cmake -S:*)", + "Bash(ctest:*)", + "Bash(cmake --install:*)", + "Bash(clang-format:*)", + "Bash(clang-tidy:*)", + "Bash(ruff check:*)", + "Bash(ruff format:*)", + "Bash(python -m pytest:*)", + "Bash(python3 -m pytest:*)", + "Bash(pytest:*)", + "Bash(valgrind:*)", + "Bash(python3 tools/dat2c.py:*)", + "Bash(python3 tools/pdf2dat.py:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4dd0b1f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,140 @@ +# AGENTS.md + +Project brief for AI coding agents (Claude Code, opencode, Qwen Code, Codex, …). +This is the single source of truth; `CLAUDE.md` imports it. Keep it current when +build commands, conventions or architecture change. + +## What this project is + +`libdedx` is a C11 library for charged-particle **stopping power** (dE/dx) +calculations — the energy loss of a charged particle per unit length of a +material. All tabulated stopping-power data is **embedded into the library**; +no external `.bin` files are needed at runtime. A `ctypes`-based Python binding +lives in `python/`. + +## Build, test, lint — the inner loop + +C library (CMake 3.21+, C11 compiler): + +```bash +cmake --preset debug # configure (also writes compile_commands.json) +cmake --build --preset debug --parallel # build into ./build +ctest --preset debug # run the C test suite +``` + +Other presets: `release` (-> `build-release/`), `coverage` (-> `build-coverage/`). + +Static analysis / formatting (both run in CI with warnings-as-errors): + +```bash +clang-format -i # style is enforced; see .clang-format +clang-tidy -p build # config in .clang-tidy, WarningsAsErrors: '*' +``` + +Memory checks (CI runs valgrind on the bethe test; do the same for memory-touching changes): + +```bash +valgrind --leak-check=full --track-origins=yes --error-exitcode=1 ./build/tests/test_bethe_ext00 +``` + +Python binding (`python/`): + +```bash +pip install -e "python[dev]" # editable install + ruff + pytest +export LIBDEDX_SO="$PWD/build/src/libdedx.so" # point ctypes at the built .so +ruff check python # lint (line-length 120, rules E/F/W) +cd python && python3 -m pytest -q # tests (use `-m pytest`, as CI does) +``` + +In Claude Code on the web, `.claude/hooks/session-start.sh` performs the +configure+build, the editable Python install, and exports `LIBDEDX_SO` +automatically, so the loop above is ready at session start. + +## Repository map + +| Path | What lives here | +|------|-----------------| +| `include/` | **Public API** headers (`dedx.h`, `dedx_error.h`, `dedx_tools.h`, `dedx_wrappers.h`, `dedx_elements.h`). The contract. | +| `src/` | Library implementation. Core engine in `dedx.c`; per-program models (`dedx_bethe.c`, `dedx_mstar.c`, `dedx_mpaul.c`); interpolation in `dedx_spline.c`; lookup/data access in `dedx_data_access.c`. | +| `src/data/embedded/` | **Generated** headers compiled into the library. Do not hand-edit. | +| `data/raw/` | Raw source tables + metadata, the regeneration inputs. | +| `tools/` | `dat2c.py` (raw tables -> embedded C headers) and `pdf2dat.py` (ICRU90 PDFs -> `.dat`). | +| `tests/` | CTest suite, one file per program/area (`test_pstar.c`, `test_bethe_ext00.c`, …). | +| `examples/` | Runnable usage samples; CI executes several of them. | +| `python/libdedx/` | ctypes binding (`_api.py`, typed stubs `__init__.pyi`, `py.typed`). | +| `docs/` | Sphinx + Doxygen sources; published to GitHub Pages. | + +## API model (orient before editing) + +- One-call convenience: `dedx_get_simple_stp(ion, target, energy, &err)`. +- Full path: allocate a `dedx_workspace`, fill a `dedx_config` (`program`, `ion`, + `target`), `dedx_load_config()`, then `dedx_get_stp()`; free both. +- A result is selected by a `(program, ion, target)` triple. Programs include + `DEDX_PSTAR`, `DEDX_ASTAR`, `DEDX_MSTAR`, `DEDX_ICRU49`, `DEDX_ICRU73`, + `DEDX_ICRU73_OLD`, `DEDX_BETHE_EXT00`, and `DEDX_ICRU` (auto-selects the + newest embedded ICRU data). +- Energies are in **MeV/nucl** throughout, including the generated data grids. + +## Conventions (enforced — see CONTRIBUTING.md) + +- **Naming/linkage:** public API `dedx_*`; shared internal helpers + `dedx_internal_*`; file-local helpers `static` with short unprefixed names. + Do **not** introduce new `_dedx_*` identifiers (leading underscore). +- **Declarations at top of block**, before any statements (Linux-kernel style). + Inner-block declarations are fine when genuinely local to that scope. +- Formatting and clang-tidy are CI gates with warnings-as-errors — run them + before claiming a change is done. + +## Gotchas an agent will otherwise get wrong + +- **Embedded data is generated, not authored.** To change tabulated values, + edit `data/raw/` and regenerate, never edit `src/data/embedded/*` directly: + ```bash + python3 tools/pdf2dat.py # refresh ICRU90 *.dat in data/raw/ + python3 tools/dat2c.py all # regenerate src/data/embedded/ headers + ``` +- **Not thread-safe.** There is no synchronization around workspace mutation in + `dedx_load_config()` / dataset loading. Never share a `dedx_workspace` across + threads without external locking. Don't assume re-entrancy. +- **Compound fallback semantics.** For compound targets, some programs + (`DEDX_PSTAR`, `DEDX_ASTAR`, `DEDX_MSTAR`) fall back to Bragg/stoichiometric + weighting when the upstream database lacks that compound. A value returned + under a program label is therefore not always a direct upstream value. Preserve + this behavior and its caveats; don't "fix" it into looking like native coverage. +- **`ASTAR` vs `ICRU_ASTAR`** are distinct datasets (same grid/targets, slightly + different values) — not duplicates. See `data/README.md`. + +## Definition of done + +1. `cmake --build --preset debug` is clean. +2. `ctest --preset debug` passes. +3. `clang-format` clean and `clang-tidy -p build` reports nothing (warnings = errors). +4. For memory-touching changes, valgrind is clean. +5. If Python is affected: `ruff check python` clean and `pytest` passes. + +## Using a self-hosted Qwen (or other local model) via opencode + +This repo is model-agnostic: Claude Code reads `CLAUDE.md` (which imports this +file); opencode and Qwen Code read this `AGENTS.md` directly. To drive the repo +with your own Qwen endpoint on custom hardware, add a custom provider to +opencode (`opencode.json` at the repo root or `~/.config/opencode/`). Point it at +your OpenAI-compatible server (vLLM / SGLang / Ollama / LM Studio) — model ids are +passed through unchanged, so they must match what your server exposes: + +```jsonc +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "my-qwen": { + "npm": "@ai-sdk/openai-compatible", + "options": { "baseURL": "http://:/v1" }, + "models": { "qwen3-coder": {} } + } + } +} +``` + +Then select `my-qwen/qwen3-coder` via `/models`. (opencode defaults Qwen to +temperature 0.55 when unset.) This file is intentionally **not** committed so the +endpoint stays your local choice; the build/test/lint commands above are all the +context any agent needs regardless of the model behind it. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d688995 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md + +Project guidance for Claude Code lives in the shared, tool-agnostic brief so +that Claude, opencode, Qwen Code and other agents all read the same thing. + +@AGENTS.md