# Wellbore Genius — MATLAB bridge

MATLAB ↔ Wellbore Genius runs over the Python SDK using MATLAB's built-in
`py.*` runtime (no MEX, no Java, no native S-function). This is the
recommended path for Simulink models that need to call our solver in the
loop, and it works on every MATLAB release ≥ R2019b on Linux / macOS /
Windows.

## One-time setup

1. Install Python ≥ 3.8 and ensure `pyenv` finds it inside MATLAB:

   ```matlab
   pyenv("Version", "/usr/bin/python3");   % or "python3" / a venv path
   ```

2. Copy or `pip install` the SDK:

   ```bash
   pip install -e public/sdk/python      # editable install for local dev
   ```

3. Export your API key (mint in **Settings → API keys**) so MATLAB picks
   it up:

   ```bash
   export DOWNHOLE_API_KEY=dh_live_...
   ```

## Hello, solver

```matlab
sdk    = py.importlib.import_module("downhole_sdk");
client = sdk.DownholeClient( ...
    pyargs("base_url", "https://wellboregenius.com", ...
           "api_key",  getenv("DOWNHOLE_API_KEY")));

spec = client.solver_spec();           % typed dict
disp(string(spec{"generated_at"}));
```

## Round-trip a reservoir model

```matlab
grdecl = fileread("models/wolfcamp.grdecl");
result = client.grdecl_import(grdecl); % { spec, summary, diagnostics, ... }
fprintf("Loaded %s\n", string(result{"summary"}));

% Hand the parsed grid to a Simulink block via the To Workspace pattern:
spec      = result{"spec"};
nx        = double(spec{"nx"});
ny        = double(spec{"ny"});
nz        = double(spec{"nz"});
permMd    = double(py.array.array("d", spec{"permMd"}));
porosity  = double(py.array.array("d", spec{"porosity"}));
```

## Drive a non-planar 3D run in the loop

```matlab
function out = nonPlanarStep(advanceFt)
    persistent client
    if isempty(client)
        sdk    = py.importlib.import_module("downhole_sdk");
        client = sdk.DownholeClient(pyargs( ...
            "base_url", "https://wellboregenius.com", ...
            "api_key",  getenv("DOWNHOLE_API_KEY")));
    end
    body = struct( ...
        "halfLengthFt",  500, "halfHeightFt", 150, "elementSizeFt", 25, ...
        "ePrimePsi",     5e6, "advanceFt",     advanceFt, ...
        "maxKinkDeg",    20,  "steps",         1);
    resp = client.non_planar_3d_run(body);
    out  = struct(resp);   % usable in From Workspace / Simulink Bus
end
```

Wrap the helper above inside a **MATLAB Function** block to call the
solver every Simulink major step. The Python SDK is stateless (each call
is one HTTP round-trip), so the block is automatically safe for
fixed-step solvers — there is no implicit MATLAB ↔ MATLAB session to
manage.

## What we do **not** ship today

- A native S-function (`.mexa64` / `.mexw64`). Customer demand has been
  zero so far — the `py.*` bridge above covers Simulink Coder and Real-
  Time Workshop without the Windows-only build chain. Open a feature
  request if you need code generation on the MATLAB side.
- A MATLAB toolbox (`.mltbx`). The two snippets above are the entire
  surface area today — there is nothing to bundle.

## Zero-dependency MATLAB examples (no Python bridge)

If you only need the JSON HTTP API — no in-loop Simulink — the scripts
under `examples/` use MATLAB's built-in `webread` / `weboptions` and
mirror the PowerShell workflow 1-for-1.

### Step-by-step setup (5 minutes)

1. **Mint an API key.** In the web app open **Settings → API keys →
   "New key"**, copy the `dh_live_...` string once (it isn't shown again),
   and note whether you're on the published site
   (`https://wellboregenius.com`) or a preview URL
   (`https://project--<PROJECT_ID>-dev.lovable.app`).

2. **Clone or download this repo** so MATLAB can see the scripts:

   ```bash
   git clone https://github.com/<your-fork>/downhole-dynamics-buddy.git
   ```

3. **Start MATLAB (R2020b+) and add the examples folder to the path** —
   `addpath` scopes to the current session, `savepath` persists it:

   ```matlab
   addpath('public/sdk/matlab/examples');
   savepath;                                 % optional — persists across sessions
   ```

4. **Store the API key for this MATLAB session.** Pick one of three
   options — all three end up as `DOWNHOLE_API_KEY` / `DOWNHOLE_BASE_URL`
   env vars that every script reads via `getenv()`.

   **Option A — hidden-input helper (interactive).** Never lands in your
   command history:

   ```matlab
   setDownholeApiKey();                      % prompts for DOWNHOLE_API_KEY
   setDownholeApiKey('-verify');             % round-trips /solver-spec to confirm
   ```

   **Option B — `.env` file (recommended for repeat sessions).** Copy the
   template once, edit it, then load it at the top of every MATLAB
   session (or from your `startup.m`):

   ```matlab
   copyfile('public/sdk/matlab/examples/.env.example', ...
            'public/sdk/matlab/examples/.env');   % once
   edit    public/sdk/matlab/examples/.env       % paste your dh_live_... key
   loadDownholeEnv();                            % every session
   ```

   `loadDownholeEnv` auto-discovers `./.env` next to the examples folder
   (then `pwd`, then `.env.example` as a last resort). Add `.env` to
   `.gitignore` — the template is safe to commit, real keys are not.

   **Option C — shell environment variables.** Set them **before**
   launching MATLAB:

   ```bash
   export DOWNHOLE_API_KEY=dh_live_...
   export DOWNHOLE_BASE_URL=https://wellboregenius.com   # optional; this is the default
   ```


5. **Run your first end-to-end call** — should print an `[ok] N rows …`
   line within a second or two:

   ```matlab
   spec = getSolverSpecReport();             % console summary only
   ```

6. **Save reports and drive CI** once step 5 works:

   ```matlab
   getSolverSpecReport('Format', 'both', ...
       'JsonPath', 'reports/solver-spec.json', ...
       'CsvPath',  'reports/solver-spec.csv');

   runSolverSpecCiGate('Format', 'csv', ...  % exits 0/2/3/4 for CI pipelines
       'CsvPath', 'reports/solver-spec.csv');
   ```

7. **Try the other examples** in any order — each is a single-file
   function under `examples/`. See the table below for the full menu.

**Troubleshooting**

- `DOWNHOLE_API_KEY is not set` → re-run `setDownholeApiKey()` or
  `loadDownholeEnv()` (env vars exported outside MATLAB after launch
  aren't visible; restart MATLAB, use the helper, or load a `.env`).

- `HTTP 401 unauthorized` → key was revoked or you're pointing at the
  wrong `BaseUrl`. Re-mint in **Settings → API keys** and re-check
  `DOWNHOLE_BASE_URL`.
- `Undefined function 'getSolverSpecReport'` → run step 3 again — the
  `examples/` folder isn't on the MATLAB path.

### Script menu



| Script | Mirrors PowerShell | What it does |
| --- | --- | --- |
| [`examples/setDownholeApiKey.m`](./examples/setDownholeApiKey.m) | `Set-DownholeApiKey.ps1` | Interactive `DOWNHOLE_API_KEY` prompt (hidden input via Swing) + optional `DOWNHOLE_BASE_URL`. Session-scoped `setenv`. Supports `'-verify'` / `'-clear'`. |
| [`examples/loadDownholeEnv.m`](./examples/loadDownholeEnv.m) | — (MATLAB-only) | Parses a `.env` file (`KEY=VALUE` lines, `#` comments) and calls `setenv()` so every other example picks the values up. Auto-discovers `./.env` beside the examples folder; skips placeholder values from `.env.example`. |
| [`examples/smokeTestSolverSpec.m`](./examples/smokeTestSolverSpec.m) | [`examples/Invoke-SmokeTest.ps1`](./examples/Invoke-SmokeTest.ps1) | Minimal `/solver-spec` round-trip for troubleshooting. Prints `[env]`/`[http]`/`[json]`/`[check]`/`[ok]` lines (base URL, key length + prefix, latency, `generatedAt`, `count`, first 3 row ids, envelope validation) and returns a boolean. Maps 401/403/404/timeout errors to actionable hints. Never prints the key. The PowerShell wrapper resolves `DOWNHOLE_API_KEY` / `DOWNHOLE_BASE_URL` from session, `.env`, User, or Machine scope (in that order; `-PreferEnvFile` inverts), forwards them to the child `matlab.exe` via Process scope only, and calls `matlab -batch` with correct double-quoted payload + MATLAB single-quote-escaped `addpath()` so spaces (Program Files, OneDrive) don't break resolution. Exit code mirrors the MATLAB boolean (0 = PASS, 1 = FAIL). |



| [`examples/getSolverSpecReport.m`](./examples/getSolverSpecReport.m) | `Get-SolverSpecReport.ps1` | Runs `GET /api/public/sdk/v1/solver-spec` end-to-end, prints the same one-line summary, writes JSON and/or CSV with the identical column order (`id,title,status,coupling,newtonWired,newtonWiredDetail,today,roadmap`), and offers a `'FailOnNextUp', true` CI gate. |
| [`examples/exportMarketplacePresets.m`](./examples/exportMarketplacePresets.m) | `Export-MarketplacePresets.ps1` | Round-trips a `MarketplacePresetEnvelope` bundle through `POST /api/public/sdk/v1/marketplace/bundle` and writes the server-normalized `{version:1, entries}` JSON to disk. |
| [`examples/importMarketplacePresets.m`](./examples/importMarketplacePresets.m) | `Import-MarketplacePresets.ps1` | Pulls a bundle from a file or URL (`'Remote', true`), validates + normalizes it through the same endpoint (accepts canonical / bare-array / single-envelope shapes), and writes a canonical bundle ready for /marketplace's drag-and-drop import. Supports `'FailOnRejected', true` for CI. |
| [`examples/runNonPlanar3d.m`](./examples/runNonPlanar3d.m) | `Invoke-NonPlanar3dRun.ps1` | Runs `POST /api/public/sdk/v1/non-planar-3d/run` end-to-end with the same defaults, prints step-count + final-mesh summary, and writes JSON and/or CSV (`step,advanceFt,kinkAppliedDeg,meanApertureIn,meanNetPressurePsi,tipCount`) byte-comparable to the PowerShell writer. |
| [`examples/runSolverSpecCiGate.m`](./examples/runSolverSpecCiGate.m) | — (MATLAB-only) | CI-ready wrapper around `getSolverSpecReport`. Exits `0` on pass, `2` when any row is `next-up`, `3` on schema drift (missing required fields, unknown status, count mismatch, CSV header drift), `4` on network/auth. Designed for `matlab -batch`. |

```matlab
% One-time (this MATLAB session)
setDownholeApiKey();                                   % prompts, input hidden

% CSV byte-comparable to the PowerShell writer:
getSolverSpecReport('Format', 'both', ...
    'JsonPath', 'reports/solver-spec.json', ...
    'CsvPath',  'reports/solver-spec.csv');

% CI gate — throws if any scoreboard row is still "next-up"
getSolverSpecReport('FailOnNextUp', true);

% Round-trip a marketplace bundle through the validator
exportMarketplacePresets('my-presets-raw.json', 'my-presets-normalized.json');

% Pull a shared bundle from a URL and prep for local import
importMarketplacePresets( ...
    'https://gist.githubusercontent.com/user/id/raw/basin.json', ...
    'ready-to-import.json', 'Remote', true, 'FailOnRejected', true);

% Non-planar 3D end-to-end -> per-step history + final-mesh summary + CSV
runNonPlanar3d('Steps', 12, 'Format', 'both', ...
    'JsonPath', 'reports/np3d.json', 'CsvPath', 'reports/np3d.csv');
```

### CI gate (matlab -batch)

```bash
matlab -batch "addpath('public/sdk/matlab/examples'); \
               runSolverSpecCiGate('Format','csv', \
                   'CsvPath','reports/solver-spec.csv')"
echo "exit=$?"   # 0=pass  2=next-up  3=schema drift  4=network/auth
```

`runSolverSpecCiGate` forces `Strict=true` + `FailOnNextUp=true` and
maps each error identifier from `getSolverSpecReport` onto a stable
POSIX exit code so shell pipelines / GitHub Actions can branch on the
failure mode without parsing MATLAB stack traces.

### GitHub Actions example

A ready-to-copy workflow lives at
[`examples/ci/solver-spec-gate.yml`](./examples/ci/solver-spec-gate.yml).
It uses [`matlab-actions/setup-matlab`](https://github.com/matlab-actions/setup-matlab)
+ [`matlab-actions/run-command`](https://github.com/matlab-actions/run-command)
to run `runSolverSpecCiGate` on every push / PR (plus a daily 06:00 UTC
canary), then uploads the CSV as a build artifact.

1. Copy the file to `.github/workflows/solver-spec-gate.yml` in your repo.
2. Under **Settings → Secrets and variables → Actions → New repository
   secret**, add `DOWNHOLE_API_KEY` (required) and, if you're pointing at
   a preview deployment, `DOWNHOLE_BASE_URL` (optional). Both are
   injected as `env:` on the MATLAB step only — never hard-code the key
   in the workflow file.
3. The MATLAB step propagates `runSolverSpecCiGate`'s exit code, so the
   job fails with the same 0/2/3/4 semantics documented above.

### Windows setup (PowerShell + `matlab -batch`)

The examples work the same on Windows, but shells, path separators, and
env-var scopes differ from macOS/Linux. Use the recipes below verbatim.

**1. Put `matlab.exe` on your PATH (one-time).** The MATLAB installer
does not do this by default. In an elevated **PowerShell**:

```powershell
# Adjust the version folder to match your install (R2024a shown).
$matlabBin = 'C:\Program Files\MATLAB\R2024a\bin'
[Environment]::SetEnvironmentVariable(
  'Path',
  ([Environment]::GetEnvironmentVariable('Path','Machine') + ';' + $matlabBin),
  'Machine')
# Open a NEW PowerShell window, then verify:
matlab -help | Select-Object -First 1     # -> "MATLAB - The Language of Technical Computing"
```

**2. Store the API key.** Pick one — all three end up as `getenv()`
inside MATLAB:

```powershell
# Option A - hidden-input helper (session only, safest).
matlab -batch "addpath('public\sdk\matlab\examples'); setDownholeApiKey()"

# Option B - persist for the current Windows user (survives reboots,
# visible to every new PowerShell / CMD / MATLAB window).
[Environment]::SetEnvironmentVariable('DOWNHOLE_API_KEY',  'dh_live_...',                'User')
[Environment]::SetEnvironmentVariable('DOWNHOLE_BASE_URL', 'https://wellboregenius.com', 'User')
# Open a NEW shell after setting — already-open windows won't see the value.

# Option C - .env file loaded from MATLAB.
Copy-Item public\sdk\matlab\examples\.env.example `
          public\sdk\matlab\examples\.env
notepad   public\sdk\matlab\examples\.env         # paste your dh_live_... key
# Then inside MATLAB:  loadDownholeEnv();
```

Verify without revealing the key:

```powershell
matlab -batch "addpath('public\sdk\matlab\examples'); setDownholeApiKey('-verify')"
```

**3. Run `matlab -batch` from PowerShell.** Backslashes for paths, and
wrap the whole command in **double quotes**; MATLAB itself uses
**single quotes** for string literals — the two nest cleanly:

```powershell
# One-liner - CSV goes to reports\solver-spec.csv relative to the current dir.
matlab -batch "addpath('public\sdk\matlab\examples'); `
               runSolverSpecCiGate('Format','csv', `
                   'CsvPath','reports\solver-spec.csv')"
Write-Host "exit=$LASTEXITCODE"   # 0=pass  2=next-up  3=schema drift  4=network/auth
```

Notes:

- **Line continuation.** PowerShell uses a trailing backtick (`` ` ``),
  **not** a backslash. In `cmd.exe` use `^` instead. Or keep the whole
  invocation on one line to avoid the issue entirely.
- **Path separators.** Use `\` in the shell (`public\sdk\...`), but
  inside MATLAB string literals either `\` or `/` works — MATLAB
  normalizes both. Prefer `fullfile('reports','solver-spec.csv')` in
  scripts to stay portable.
- **Spaces in paths.** Wrap the argument to `-batch` in double quotes as
  shown; if `CsvPath` itself contains spaces, escape the inner single
  quotes: `'CsvPath','C:\My Reports\solver-spec.csv'`.
- **Exit code.** `matlab -batch` propagates the MATLAB `exit(N)` value
  as the process exit code, readable via `$LASTEXITCODE` in PowerShell
  or `%ERRORLEVEL%` in `cmd.exe`.

**4. Scheduled Task (Windows equivalent of cron).** Three drop-in
options, in order of "least ceremony" to "most repeatable":

**Option A — inline `New-ScheduledTaskAction` (quick + dirty).**

```powershell
$action  = New-ScheduledTaskAction -Execute 'matlab.exe' `
             -Argument "-batch ""addpath('C:\repos\downhole\public\sdk\matlab\examples'); runSolverSpecCiGate('Format','csv','CsvPath','C:\reports\solver-spec.csv')""" `
             -WorkingDirectory 'C:\repos\downhole'
$trigger = New-ScheduledTaskTrigger -Daily -At 6am
Register-ScheduledTask -TaskName 'downhole-solver-spec-gate' `
                       -Action $action -Trigger $trigger `
                       -Description 'Wellbore Genius solver-spec CI gate'
```

**Option B — one-shot register script (recommended).** Uses the
[`Invoke-CiGate.ps1`](./examples/Invoke-CiGate.ps1) wrapper so
`DOWNHOLE_API_KEY` is resolved from session / `.env` / User / Machine
scope (in that order), forwarded Process-scoped to `matlab.exe`, and
never lands in the Task XML or Event Log. The wrapper also propagates
the CI gate's full exit ladder (0 PASS, 2 NEXT-UP, 3 DRIFT, 4 NETWORK,
1 LAUNCH-FAIL) and appends a one-line status to a rolling log:

```powershell
# Default: 06:00 daily under the current user, CSV → C:\reports\wbg\
.\examples\ci\Register-SolverSpecGateTask.ps1

# Custom time, prefer .env credentials over User scope, overwrite existing
.\examples\ci\Register-SolverSpecGateTask.ps1 `
    -TimeOfDay '02:30' -PreferEnvFile -Force

# Manual dry-run + last-result check
Start-ScheduledTask         -TaskName 'WBG\SolverSpecGate'
Get-ScheduledTaskInfo       -TaskName 'WBG\SolverSpecGate' |
  Select-Object LastRunTime, LastTaskResult, NextRunTime
```

**Option C — importable XML template.** Version-control-friendly copy
of the same task shape at
[`examples/ci/solver-spec-gate.ScheduledTask.xml`](./examples/ci/solver-spec-gate.ScheduledTask.xml).
Edit the three `DOMAIN\username` / path placeholders, then import:

```powershell
schtasks /Create /XML `
  "public\sdk\matlab\examples\ci\solver-spec-gate.ScheduledTask.xml" `
  /TN "WBG\SolverSpecGate"

# Or, purely from PowerShell:
Register-ScheduledTask -TaskName 'WBG\SolverSpecGate' `
  -Xml (Get-Content -Raw .\examples\ci\solver-spec-gate.ScheduledTask.xml)
```

All three options inherit `DOWNHOLE_API_KEY` from the **User** scope you
set in step 2 (or from `.env` when the wrapper is used with
`-PreferEnvFile`). To scope the task to a service account, set the env
var in **Machine** scope from an elevated PowerShell and register with
`-User 'DOMAIN\svc-account' -RunLevel Highest`.



**5. Troubleshooting (Windows-specific)**

- `'matlab' is not recognized as an internal or external command` →
  step 1 didn't take effect. Open a **new** shell, or use the full path:
  `& 'C:\Program Files\MATLAB\R2024a\bin\matlab.exe' -batch ...`.
- `DOWNHOLE_API_KEY is not set` inside MATLAB even though `$Env:DOWNHOLE_API_KEY`
  shows the value → you set it in **Process** scope only. Persist it
  with `[Environment]::SetEnvironmentVariable(..., 'User')` and open a
  new shell.
- Paths with **OneDrive / spaces** (e.g.
  `C:\Users\Jane\OneDrive - ACME\downhole`) → wrap the whole `-batch`
  string in double quotes and use MATLAB's `fullfile()` inside; avoid
  backslash-escaping the space.
- **Line-ending noise** in `reports\*.csv` when consumed by non-Windows
  tools → MATLAB writes LF by default; if a downstream tool needs CRLF,
  post-process with `(Get-Content x.csv) | Set-Content x.csv`.

**6. Corporate proxies, TLS interception, and custom CAs**

Enterprise networks almost always sit behind an authenticating proxy
(Zscaler, Netskope, BlueCoat, Palo Alto, Forcepoint, on-prem Squid, …)
that terminates TLS and re-signs traffic with an internal root CA. Both
PowerShell (`webread` / `Invoke-RestMethod`) and MATLAB (`webread` +
Java `HttpsURLConnection`) will fail closed until they trust that CA
and know the proxy URL. Symptoms and fixes:

**Common error → probable cause**

| Error message | Meaning | First fix to try |
| --- | --- | --- |
| `webread: The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.` | Proxy is re-signing TLS with a CA MATLAB doesn't trust. | Install the corporate CA (see below). |
| `unable to get local issuer certificate` / `SEC_ERROR_UNKNOWN_ISSUER` | Same as above, reported by the underlying OpenSSL/Java stack. | Install the corporate CA. |
| `HTTP 407 Proxy Authentication Required` | Proxy requires credentials MATLAB didn't send. | Configure MATLAB proxy in **Preferences → Web** or set `HTTPS_PROXY`. |
| `Could not resolve host: wellboregenius.com` from MATLAB but curl works | MATLAB is bypassing the system proxy. | Copy IE/Edge proxy settings into MATLAB Preferences. |
| `PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException` | Java (used by MATLAB `webread`) doesn't trust the proxy CA — cert lives in the OS store but not in MATLAB's JRE cacerts. | Import the CA into MATLAB's `cacerts` (see step 2 below). |
| `TLS handshake timeout` on port 443 only | Proxy is dropping non-CONNECT traffic. | Add `wellboregenius.com` (and the `project--*.lovable.app` preview host) to the proxy allow-list. |
| Sporadic `SSL: certificate subject name does not match target host` for preview URLs | Proxy is rewriting SNI on wildcard subdomains. | Ask netsec to pin the wildcard `*.lovable.app` — or set `weboptions('CertificateFilename','')` **temporarily** for reproduction only. |

**1. Point PowerShell + MATLAB at the corporate proxy.**

```powershell
# One-shot for the current session (both scripts pick these up)
$Env:HTTPS_PROXY = 'http://proxy.acme.corp:8080'
$Env:HTTP_PROXY  = 'http://proxy.acme.corp:8080'
$Env:NO_PROXY    = 'localhost,127.0.0.1,.acme.corp'

# Persist for the current Windows user (survives reboots)
[Environment]::SetEnvironmentVariable('HTTPS_PROXY', 'http://proxy.acme.corp:8080', 'User')
[Environment]::SetEnvironmentVariable('HTTP_PROXY',  'http://proxy.acme.corp:8080', 'User')
[Environment]::SetEnvironmentVariable('NO_PROXY',    'localhost,127.0.0.1,.acme.corp', 'User')
```

MATLAB also has a GUI setting that overrides env vars: **Home → Preferences → MATLAB → Web → Use a proxy server to connect to the Internet**. Fill in host / port / username / password (stored in `matlab.settings`, not the shell). If auth uses NTLM / Kerberos, front the proxy with [Cntlm](http://cntlm.sourceforge.net/) or [px-proxy](https://github.com/genotrance/px) on `localhost:3128` and point `HTTPS_PROXY` there — MATLAB doesn't speak NTLM natively.

**2. Install the corporate root CA in the right trust stores.**

TLS-intercepting proxies present traffic signed by an *internal* root. Windows, PowerShell, MATLAB, and Java each look in a **different** store — you generally need to import the CA into two of them.

```powershell
# Grab the intercepted cert chain from the proxy (adjust the URL)
$cert = (Invoke-WebRequest -Uri 'https://wellboregenius.com' `
           -UseBasicParsing).BaseResponse   # then inspect $cert.Certificate

# Or, if netsec gave you a .crt / .cer / .pem file:
$caPath = 'C:\certs\acme-root-ca.crt'

# 2a. Windows trust store — fixes PowerShell / .NET / curl.exe
Import-Certificate -FilePath $caPath `
    -CertStoreLocation Cert:\LocalMachine\Root   # requires elevation
# (Or Cert:\CurrentUser\Root without admin, current user only.)

# 2b. MATLAB's own PEM bundle — fixes `webread` in MATLAB R2016b+
$matlabCerts = fullfile(matlabroot,'sys','certificates','ca','rootcerts.pem')
# Append to that file (BACK IT UP FIRST), or point MATLAB elsewhere:
#   weboptions('CertificateFilename','C:\certs\combined-bundle.pem')

# 2c. MATLAB's bundled JRE (only if you hit PKIX errors) — fixes Java HTTPS
$keytool = "$([Environment]::GetEnvironmentVariable('MATLABROOT'))\sys\java\jre\win64\jre\bin\keytool.exe"
& $keytool -importcert -trustcacerts -noprompt `
    -alias acme-root-ca -file $caPath `
    -keystore "$([Environment]::GetEnvironmentVariable('MATLABROOT'))\sys\java\jre\win64\jre\lib\security\cacerts" `
    -storepass changeit
```

The MATLAB-bundle path (2b) fixes `webread`; the JRE keystore (2c) is only needed when you see `PKIX path building failed` (that path is exercised by some toolboxes and by RESTful web-service calls under the hood). **Do not delete** the shipped `rootcerts.pem` / `cacerts`; append.

**3. Verify the trust chain end-to-end.**

```powershell
# a) OS trust: does .NET see it?
Invoke-RestMethod -Uri 'https://wellboregenius.com/api/public/sdk/v1/solver-spec' `
    -Headers @{ Authorization = "Bearer $Env:DOWNHOLE_API_KEY" }

# b) MATLAB trust: does `webread` see it? (run inside MATLAB)
opts = weboptions('HeaderFields', {'Authorization', ['Bearer ' getenv('DOWNHOLE_API_KEY')]}, ...
                  'Timeout', 30);
webread('https://wellboregenius.com/api/public/sdk/v1/solver-spec', opts);

# c) End-to-end via the smoke test — one line, structured output
.\Invoke-SmokeTest.ps1
```

If (a) succeeds but (b) fails, the CA is missing from MATLAB's PEM bundle (step 2b). If both fail, the CA is missing from Windows (step 2a) or the proxy env vars are wrong (step 1).

**4. Emergency escape hatches (reproduction / non-prod only).**

Never leave these on in production:

```matlab
% Disable TLS verification for a SINGLE call, to prove interception is the cause
opts = weboptions('CertificateFilename', '', ...
                  'HeaderFields', {'Authorization', ['Bearer ' getenv('DOWNHOLE_API_KEY')]});
webread('https://wellboregenius.com/api/public/sdk/v1/solver-spec', opts);
```

```powershell
# PowerShell 5.1 equivalent — DISABLES verification globally in the session
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
```

If either of these makes the smoke test pass, TLS interception is confirmed and the real fix is step 2 above. Re-enable verification (`CertificateFilename` back to the default, restart PowerShell) before running anything against production.

**5. When the corporate policy forbids installing the CA at all.**

- Ask netsec to add `wellboregenius.com` and `*.lovable.app` to the proxy's TLS-**bypass** list (traffic tunnels straight through without re-signing).
- Or route the CI gate through a jump host that already trusts the public CA.
- Lovable cannot MITM its own certificate for you — if traffic must be inspected, the corporate CA is the only supported path.





### macOS / Linux setup (bash / zsh + `matlab -batch`)

Same shape as the Windows section — five numbered steps plus a
Troubleshooting block — so recipes translate line-for-line between
platforms.

**1. Put `matlab` on your PATH (one-time).** The installer does NOT
add it on macOS or most Linux distros.

```bash
# macOS (Apple silicon / Intel — MATLAB.app lives in /Applications)
echo 'export PATH="/Applications/MATLAB_R2024a.app/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# Linux (Ubuntu / RHEL — default install prefix)
echo 'export PATH="/usr/local/MATLAB/R2024a/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Verify (both platforms)
which matlab && matlab -batch "disp('ok')"
```

If MATLAB lives elsewhere (jamf-managed / network install / home dir),
substitute the actual `matlabroot`; you can query it from an existing
MATLAB session with `disp(matlabroot)`.

**2. Store the API key.** Pick one — all three end up as `getenv()`
inside MATLAB, exactly like Windows.

```bash
# 2a. Session-only (safest, dies with the shell)
matlab -batch "setDownholeApiKey()"

# 2b. Persist for your user (survives reboots + login shells)
# --- zsh (macOS default since 10.15) ---
cat >> ~/.zshenv <<'EOF'
export DOWNHOLE_API_KEY="dh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export DOWNHOLE_BASE_URL="https://wellboregenius.com"
EOF
chmod 600 ~/.zshenv                   # keep the key off other users' eyes
source ~/.zshenv

# --- bash (most Linux distros) ---
cat >> ~/.bash_profile <<'EOF'
export DOWNHOLE_API_KEY="dh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export DOWNHOLE_BASE_URL="https://wellboregenius.com"
EOF
chmod 600 ~/.bash_profile
source ~/.bash_profile

# 2c. .env file loaded from MATLAB (avoids leaking to child processes)
cp examples/.env.example examples/.env
chmod 600 examples/.env               # tighten perms; never commit .env
$EDITOR examples/.env                 # fill in the key
matlab -batch "addpath('$PWD/examples'); loadDownholeEnv(); getSolverSpecReport()"
```

macOS Keychain / Linux `secret-tool` users: read the value at shell
startup with `security find-generic-password -w -s DOWNHOLE_API_KEY` or
`secret-tool lookup service DOWNHOLE_API_KEY` and `export` it — same
result.

**3. Run `matlab -batch` from bash / zsh.** POSIX quoting is *inverted*
from PowerShell — wrap the `-batch` payload in **single** quotes so the
shell doesn't expand `$`, and use double quotes only if you need shell
interpolation. MATLAB itself still uses **single** quotes for its own
string literals.

```bash
# Simplest form — one MATLAB statement, no shell expansion
matlab -batch 'runSolverSpecCiGate("Format","csv","CsvPath","/tmp/wbg/solver-spec.csv")'

# Multi-statement — chain with ';' inside the same single-quoted string
matlab -batch 'addpath(fullfile(pwd,"examples")); ok = smokeTestSolverSpec(); exit(double(~ok));'

# Interpolate a shell variable — switch the OUTER quote to double, keep
# MATLAB's inner strings single-quoted (Bash won't touch them)
REPORT_DIR="$HOME/reports/wbg"
mkdir -p "$REPORT_DIR"
matlab -batch "addpath(fullfile(pwd,'examples')); runSolverSpecCiGate('Format','csv','CsvPath','$REPORT_DIR/solver-spec.csv')"
```

Path-quoting rules that keep the shell out of your way:

- Prefer `fullfile()` inside MATLAB over hand-concatenated paths — it
  picks the right separator on every OS and tolerates trailing slashes.
- Wrap paths with **spaces** (`~/Library/CloudStorage/OneDrive - ACME/…`)
  in MATLAB single quotes; escape an embedded single quote by doubling
  it (`'ACME''s dir'`), not with a backslash — MATLAB is not C.
- The exit status of `matlab -batch` is the value passed to
  `exit(N)`, readable as `$?` in the parent shell — mirror this for
  CI gates: `matlab -batch '...'; echo "exit=$?"`.
- Trailing newlines: `matlab -batch` emits an extra blank line on
  success. Pipe through `sed '/^$/d'` if a downstream log parser
  chokes on it.

**4. Cron / launchd (Linux/macOS equivalent of Scheduled Tasks).**
Nightly runs of the CI gate:

```bash
# --- Linux (crontab -e) ---
# Run daily at 06:00, log everything, forward the User-scope key.
0 6 * * *  bash -lc 'DOWNHOLE_API_KEY="$(cat ~/.config/downhole/api-key)" \
  matlab -batch "addpath(''/opt/downhole/public/sdk/matlab/examples''); \
                 runSolverSpecCiGate(''Format'',''csv'',''CsvPath'',''/var/log/wbg/solver-spec.csv'')" \
  >> /var/log/wbg/ci-gate.log 2>&1'
```

`bash -lc` gives cron the same login-shell env (PATH, DOWNHOLE_*) your
interactive session sees; without it cron's stripped-down env will
report `'matlab' not found` and `DOWNHOLE_API_KEY is not set`.

```xml
<!-- macOS launchd: ~/Library/LaunchAgents/com.acme.wbg.ci-gate.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
                       "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>            <string>com.acme.wbg.ci-gate</string>
    <key>ProgramArguments</key>
    <array>
      <string>/bin/zsh</string>
      <string>-lc</string>
      <string>matlab -batch 'addpath("/opt/downhole/public/sdk/matlab/examples"); runSolverSpecCiGate("Format","csv","CsvPath","/Users/Shared/wbg/solver-spec.csv")'</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
      <key>DOWNHOLE_API_KEY</key>  <string>dh_live_xxxx</string>
      <key>DOWNHOLE_BASE_URL</key> <string>https://wellboregenius.com</string>
    </dict>
    <key>StartCalendarInterval</key>
    <dict><key>Hour</key><integer>6</integer><key>Minute</key><integer>0</integer></dict>
    <key>StandardOutPath</key>   <string>/Users/Shared/wbg/ci-gate.log</string>
    <key>StandardErrorPath</key> <string>/Users/Shared/wbg/ci-gate.log</string>
  </dict>
</plist>
```

Load with `launchctl load ~/Library/LaunchAgents/com.acme.wbg.ci-gate.plist`.
Prefer reading the key from the login Keychain in the `ProgramArguments`
shell one-liner over hard-coding it in `EnvironmentVariables` (the
`.plist` is world-readable).

**5. Troubleshooting (macOS / Linux-specific)**

- `matlab: command not found` → step 1 didn't take effect for this shell
  (login vs interactive vs non-interactive). Fix by re-`source`-ing the
  rc file, or invoke the full path:
  `/Applications/MATLAB_R2024a.app/bin/matlab -batch ...`.
- `DOWNHOLE_API_KEY is not set` inside MATLAB despite `echo $DOWNHOLE_API_KEY`
  showing the value → cron / launchd / systemd-timer strips env. Wrap
  the launcher in `bash -lc '...'` or set the var explicitly in the job
  definition (see step 4). For zsh, put exports in `~/.zshenv` (not
  `~/.zshrc`) so **non-interactive** MATLAB sessions inherit them.
- macOS **Gatekeeper** blocks the first `matlab` launch under a new
  user → run `sudo spctl --add /Applications/MATLAB_R2024a.app` once,
  or open MATLAB once via Finder to accept the notarization prompt.
- macOS **App Translocation** on quarantined installs makes
  `matlabroot` point at `/private/var/folders/…` and breaks `.env`
  discovery next to your scripts → drag `MATLAB.app` out of `~/Downloads`
  into `/Applications` before first launch, or `xattr -dr com.apple.quarantine /Applications/MATLAB_R2024a.app`.
- Linux **display-less servers** (`Cannot connect to X server`) → add
  `-nodisplay -nosplash -nodesktop` before `-batch`; `matlab -batch`
  implies most of these but not `-nodisplay` on every release.
- **Paths with spaces / iCloud / OneDrive** (`~/Library/CloudStorage/…`,
  `/mnt/c/Users/…` under WSL) → use `fullfile()` inside MATLAB and
  keep the outer shell quoting single-quoted; never rely on
  `\ ` backslash-escaped spaces once the string is inside MATLAB.
- **Line-ending noise** in `reports/*.csv` — inverse of the Windows
  case: MATLAB writes LF, so no post-processing needed unless a
  Windows tool needs CRLF (`unix2dos reports/*.csv`).
- `SSL certificate problem: unable to get local issuer certificate` —
  same corporate-proxy story as Windows section 6, but the trust
  stores live at `/etc/ssl/certs/` (Linux) or the login Keychain
  (macOS). For MATLAB's PEM bundle, append the corporate root to
  `${MATLABROOT}/sys/certificates/ca/rootcerts.pem`; for the bundled
  JRE, use the platform `keytool`:
  `"$(matlab -batch 'disp(matlabroot)' | tr -d '\n')/sys/java/jre/glnxa64/jre/bin/keytool"`
  (Linux) or `.../maci64/jre/Contents/Home/bin/keytool` (macOS).




## See also

- `public/sdk/python/README.md` — every Wellbore Genius SDK endpoint the
  bridge exposes (`solver_spec`, `non_planar_3d_run`,
  `parent_child_analyze`, `run_workflow`, `grdecl_import`, …).
- `public/sdk/powershell/README.md` — the PowerShell counterparts of the
  scripts above, plus `Invoke-ParentChildAnalyze.ps1` and
  `Invoke-NonPlanar3dRun.ps1`.
- `/interop` — high-level matrix of every supported integration.
- `docs/fracpro-csv-schema.md` — the documented text schema for FracPro /
  Meyer interchange that MATLAB can read with `readtable`.
