# Downhole Dynamics — Python SDK

Thin, dependency-free Python client for the public endpoints at
`/api/public/sdk/v1/*`. Standard library only (`urllib`).

## Installation

The SDK is a single file (`downhole_sdk.py`). Pick the option that fits your
workflow:

### Option 1 — Drop-in (no install)

```bash
curl -O https://wellboregenius.com/sdk/python/downhole_sdk.py
```

Then `from downhole_sdk import DownholeClient` from any script in the same
folder.

### Option 2 — Clone the repo

```bash
git clone <repo-url> downhole && cd downhole/public/sdk/python
```

### Requirements

- Python ≥ 3.8
- No third-party packages required.

## Authentication

Mint an API key in the app under **Settings → API keys**, then export it:

```bash
export DOWNHOLE_API_KEY="dh_live_..."
# optional — override the default base URL
export DOWNHOLE_BASE_URL="https://wellboregenius.com"
```

## Run the non-planar 3D example

The example lives in [`examples/run_non_planar_3d.py`](examples/run_non_planar_3d.py).
It calls `POST /api/public/sdk/v1/non-planar-3d/run` and prints the resulting
mesh size, per-step history (`tipKinkDeg`, `maxOpeningIn`, weld counts), and
aggregate metrics.

```bash
cd public/sdk/python
python examples/run_non_planar_3d.py --steps 10
```

### Useful flags

| Flag                        | Default | Description                                                                             |
| --------------------------- | ------- | --------------------------------------------------------------------------------------- |
| `--steps`                   | `8`     | Number of propagation steps                                                             |
| `--half-length-ft`          | `200`   | Initial fracture half-length                                                            |
| `--element-size-ft`         | `10`    | Triangulation element size                                                              |
| `--advance-ft`              | `8`     | Tip advance per step                                                                    |
| `--max-kink-deg`            | `15`    | Max kink angle per step                                                                 |
| `--e-prime-psi`             | `2.5e6` | Plane-strain modulus E′                                                                 |
| `--stress-driven-curvature` | off     | Enable σ-rotation-driven curvature                                                      |
| `--csv PATH`                | —       | Write per-step history + aggregate metrics to CSV                                       |
| `--json PATH`               | —       | Write `{inputs, mesh, history, aggregate}` bundle to JSON (`-` for stdout)              |
| `--json-full`               | off     | Include the full raw API response under `raw` in the JSON bundle                        |
| `--plot PATH`               | —       | Write a PNG with kink / opening / weld-count subplots vs step (needs `matplotlib`)      |
| `--show-plot`               | off     | Display the plot in an interactive window                                               |
| `--seed N`                  | —       | Deterministic solver seed (recorded in the manifest)                                    |
| `--manifest PATH`           | —       | Write a reproducibility manifest (cli, inputs + SHA-256, API version, key outputs, env) |
| `--api-key`                 | env     | Override `DOWNHOLE_API_KEY`                                                             |
| `--base-url`                | env     | Override `DOWNHOLE_BASE_URL`                                                            |

### Reproducibility manifest

```bash
python examples/run_non_planar_3d.py --steps 10 --seed 42 \
  --manifest out/run.manifest.json
```

The manifest (`schema: downhole.run_manifest.v1`) captures the endpoint + base
URL, the API version reported by the response (or `"unknown"`), the full
inputs payload + a stable `sha256` hash, the seed, the CLI argv, Python +
platform info, started/finished UTC timestamps, and key outputs (mesh
vertex/face counts, step count, aggregate metrics). Re-running the same
command with the same `--seed` should reproduce the same `inputsHash` and
key outputs.

### Export to CSV / JSON

```bash
# Pipe key outputs into a CSV + JSON pair for downstream reporting:
python examples/run_non_planar_3d.py --steps 10 \
  --csv out/run.csv --json out/run.json

# Stream JSON to stdout (handy for jq / scripting):
python examples/run_non_planar_3d.py --steps 10 --json - | jq '.aggregate'
```

The CSV has two sections — `# per_step_history` (one row per propagation step:
`tipKinkDeg`, `maxOpeningIn`, weld counts) and `# aggregate_metrics`
(`stepCount`, mean/max kink, max opening, total welds + removed faces, total
growth ft, mesh vertex/face counts). Missing or non-finite numerics render as
an em-dash `—`, matching the project-wide convention.

The JSON bundle contains `{inputs, mesh:{vertexCount,faceCount}, history,
aggregate}`. Add `--json-full` to also embed the raw API response under `raw`.
NaN and ±Inf are converted to `null` so the file is valid JSON.

### Plot history (matplotlib)

```bash
pip install matplotlib
python examples/run_non_planar_3d.py --steps 12 --plot out/run.png
```

Produces a 3-panel PNG: tip-kink angle [deg], max opening [in], and weld
counts (welded vertices + removed faces) vs propagation step. Use
`--show-plot` to pop up an interactive window instead of (or in addition to)
saving the file. Headless runs use the `Agg` backend automatically.

### Expected output (saved sample)

This is a verbatim run captured from the deterministic `--mock` mode so you
can diff your own results against a known-good baseline. Use the exact same
flags (`--mock --steps 10 --seed 1`) and every number below should match
byte-for-byte; if you swap `--mock` for a real API key the print layout is
identical and only the numbers change.

Command:

```
python run_non_planar_3d.py --mock --steps 10 --seed 1
```

Output:

```
→ MOCK non-planar-3d run (offline)  (steps=10, seed=1)
  mesh: 52 vertices · 49 faces
  steps: 10
  step | tipKinkDeg | maxOpeningIn | welds(v/f)
     0 |      5.816 |       0.1718 | 0/0
     1 |      6.928 |       0.1678 | 1/1
     2 |      7.887 |       0.1570 | 0/0
     3 |      8.250 |       0.1491 | 0/0
     4 |      9.197 |       0.1433 | 0/0
     5 |     10.228 |       0.1381 | 0/0
     6 |     11.239 |       0.1310 | 0/0
     7 |     11.572 |       0.1230 | 0/0
     8 |     12.553 |       0.1173 | 0/0
     9 |     13.421 |       0.1085 | 0/0

Aggregate metrics
  mean kink [deg]:   9.709
  max  kink [deg]:   13.421
  max  opening [in]: 0.1718
  total welds:       1
  total growth [ft]: 80.0
```

Sanity checks worth running against your own output:

- `steps:` matches the `--steps` you passed.
- `tipKinkDeg` rises roughly monotonically toward `--max-kink-deg`.
- `maxOpeningIn` is positive and softens as the tip advances.
- `total growth [ft] == steps × --advance-ft` (here `10 × 8.0 = 80.0`).
- A real API run keeps the same column layout — only mesh size and numbers
  differ.

## Troubleshooting

- **`ERROR 401: Unauthorized`** — check that `DOWNHOLE_API_KEY` is exported in
  the same shell that runs the script.
- **`ModuleNotFoundError: downhole_sdk`** — run the example from inside
  `public/sdk/python/` so it can locate `downhole_sdk.py` on the parent path.
- **Timeouts / TLS errors** — confirm `--base-url` matches an environment you
  have access to (production vs. preview).
