> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tetryx.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Basilisk: Basic Orbit Monte Carlo

> Run a dispersed orbital propagation study across a Lynx runner pool using Basilisk and collect trajectory artifacts.

This tutorial walks through the complete path for a Monte Carlo orbit propagation study:
submitting a dispersed batch, running it through `lynx-runner` with the Basilisk engine, streaming telemetry, and retrieving per-job trajectory artifacts.

The scenario used here is [`scenario_basic_orbit_lynx.py`](https://github.com/tetryx-io/lynx/blob/main/scenarios/aerospace/python/scenario_basic_orbit_lynx.py).

## What Lynx handles for you

You bring the simulation. Lynx handles everything around it.

|                      | Handled by Lynx                           | Provided by you                            |
| -------------------- | ----------------------------------------- | ------------------------------------------ |
| Run configurations   | Generated from your dispersion spec       |                                            |
| Job distribution     | Queued and routed across your runner pool |                                            |
| Retries              | Automatic, up to the limit you set        |                                            |
| Output directory     | Created and isolated per run              |                                            |
| Telemetry collection | Ingested and stored                       | Streamed from your scenario (optional)     |
| Early termination    | Watchdog monitors and kills on breach     | Stopping conditions defined in your config |
| Simulation physics   |                                           | Your code and models                       |
| Result files         |                                           | Written to the directory Lynx provides     |

Lynx invokes your scenario as a subprocess. Your scenario reads its parameters, runs, writes results, and exits — that's the full contract.

## How the runner launches your scenario

For each job, the Basilisk engine plugin in `lynx-runner` does the following:

1. Creates a per-job output directory
2. Starts a gRPC telemetry server on a dynamic local port
3. Spawns your Python entrypoint as a subprocess with these environment variables injected:

| Variable               | Description                                                               |
| ---------------------- | ------------------------------------------------------------------------- |
| `TETRYX_RUN_CONFIG`    | Path to a JSON file containing the sampled run configuration for this job |
| `TETRYX_TELEMETRY_URL` | gRPC endpoint to send scalar telemetry to                                 |
| `TETRYX_OUTPUT_DIR`    | Directory where your scenario should write artifacts                      |

Where the runner places the run configuration file on disk is an internal detail. Your scenario reads the path from `TETRYX_RUN_CONFIG` and does not need to know or hardcode the backing location.

4. Waits for the process to exit
5. If an early termination condition fires, sends SIGKILL to the subprocess immediately
6. Collects output files from `TETRYX_OUTPUT_DIR` and attaches them to the job result

Your scenario does not need to communicate with the Lynx API directly. Everything flows through the injected environment.

## The run configuration

`TETRYX_RUN_CONFIG` points to a JSON file with this shape:

```json theme={null}
{
  "run_id": 2,
  "parameters": {
    "semi_major_axis_m": 6983421.7,
    "eccentricity": 0.00094,
    "inclination_deg": 34.1,
    "raan_deg": 47.3,
    "arg_perigee_deg": 341.2,
    "true_anomaly_deg": 88.6,
    "duration_orbits": 0.74
  },
  "metadata": {
    "simulation_name": "Basic Orbit Tutorial",
    "simulation_version": "1.0.0",
    "duration_minutes": 90,
    "sampling_method": "latin_hypercube",
    "generated_at": "2026-04-09T00:00:00Z"
  }
}
```

The `parameters` keys and values come directly from the dispersions you defined in your batch config. Lynx samples them using the method you specified (Latin Hypercube Sampling in this tutorial).

## The scenario

`scenarios/aerospace/python/scenario_basic_orbit_lynx.py` reads the run config, builds a headless Basilisk simulation, steps through the orbit, and emits scalar telemetry at each time step.

**Reading the run config:**

```python theme={null}
import json, os
from pathlib import Path

run_config = json.loads(Path(os.environ["TETRYX_RUN_CONFIG"]).read_text())
run_id     = int(run_config.get("run_id", 0))
params     = run_config.get("parameters", {})

semi_major_axis_m = float(params.get("semi_major_axis_m", 7_000_000.0))
eccentricity      = float(params.get("eccentricity", 0.0001))
inclination_deg   = float(params.get("inclination_deg", 33.3))
# ... and so on
```

**Streaming telemetry:**

```python theme={null}
from lynx_telemetry_client import telemetry_client_from_env

client = telemetry_client_from_env()   # reads TETRYX_TELEMETRY_URL

# inside the simulation loop
client.send({
    "orbit.radius_km":         float(np.linalg.norm(r_vec) / 1000.0),
    "orbit.speed_m_s":         float(np.linalg.norm(v_vec)),
    "orbit.semi_major_axis_km": float(oe.a / 1000.0),
    "orbit.eccentricity":      float(oe.e),
    "orbit.inclination_deg":   float(oe.i * macros.R2D),
    "orbit.true_anomaly_deg":  float(oe.f * macros.R2D),
}, timestamp_ns=int(times_ns[-1]))
```

Telemetry is optional — if `TETRYX_TELEMETRY_URL` is not set the client silently no-ops. Your scenario will still run and produce artifacts.

**Writing artifacts:**

The scenario writes three files to `TETRYX_OUTPUT_DIR` before exiting:

| File                 | Contents                                                                  |
| -------------------- | ------------------------------------------------------------------------- |
| `summary.json`       | Final orbital state, sample count, execution time, success flag           |
| `trajectory.csv`     | Time-series of position and velocity vectors (one row per 10-second step) |
| `orbit_position.png` | Position components in km over time, normalised to orbital periods        |

These become the `output_files` attached to the job result, retrievable via `GET /jobs/{job_id}/results`.

## Dispersed parameters

The tutorial config (`scenarios/aerospace/configs/basic_orbit_tutorial.yaml`) disperses these orbital elements using uniform distributions:

| Parameter           | Range                 | Units         |
| ------------------- | --------------------- | ------------- |
| `semi_major_axis_m` | 6 950 000 – 7 050 000 | m             |
| `eccentricity`      | 0.00005 – 0.002       | dimensionless |
| `inclination_deg`   | 30 – 36               | deg           |
| `raan_deg`          | 40 – 55               | deg           |
| `arg_perigee_deg`   | 330 – 355             | deg           |
| `true_anomaly_deg`  | 70 – 100              | deg           |
| `duration_orbits`   | 0.70 – 0.80           | orbits        |

Each job in the batch receives a unique point sampled from this 7-dimensional space via Latin Hypercube Sampling. With LHS, 50 runs give better coverage than 50 purely random draws.

## Runner configuration

The runner needs to know which Python executable to use, where the scenario entrypoint lives, and where to write per-job output. These are set in the runner's YAML config file.

A minimal config for this tutorial (`configs/lynx-runner-basilisk.yaml`):

```yaml theme={null}
engines:
  - basilisk
capabilities:
  - basilisk
  - orbit
  - aerospace

working_directory: .
output_root: /tmp/tetryx-runs
python_executable: python3
basilisk_entrypoint: scenarios/aerospace/python/scenario_basic_orbit_lynx.py

max_concurrent_jobs: 1
heartbeat_interval_secs: 2
```

Connection credentials (the platform endpoint) are set via environment variables provided during onboarding — they are not stored in this file.

All config fields can also be set via environment variables, which is the preferred approach in containerised deployments:

| Environment variable              | Equivalent config field |
| --------------------------------- | ----------------------- |
| `LYNX_RUNNER_ENGINES`             | `engines`               |
| `LYNX_RUNNER_CAPABILITIES`        | `capabilities`          |
| `LYNX_RUNNER_PYTHON_EXECUTABLE`   | `python_executable`     |
| `LYNX_RUNNER_BASILISK_ENTRYPOINT` | `basilisk_entrypoint`   |
| `LYNX_RUNNER_OUTPUT_ROOT`         | `output_root`           |
| `LYNX_RUNNER_WORKING_DIRECTORY`   | `working_directory`     |
| `LYNX_RUNNER_MAX_CONCURRENT_JOBS` | `max_concurrent_jobs`   |

## Start a Basilisk runner

```bash theme={null}
lynx-runner \
  --config configs/lynx-runner-basilisk.yaml \
  --runner-id basilisk-runner-01 \
  --name "Basilisk Runner 01"
```

Verify it registered and is advertising the right capabilities:

```bash theme={null}
curl -s https://api.lynx.tetryx.io/runners/basilisk-runner-01 | jq '.capabilities'
```

Expected:

```json theme={null}
{
  "max_concurrent_jobs": 1,
  "supported_simulation_types": ["basilisk", "orbit", "aerospace"],
  "gpu_available": false,
  "memory_gb": 8.0,
  "cpu_cores": 4
}
```

## Submit the batch

```bash theme={null}
curl -sS \
  -X POST https://api.lynx.tetryx.io/jobs \
  -H 'content-type: application/json' \
  -d '{
    "name": "basic-orbit-tutorial",
    "runs_override": 5,
    "config": {
      "simulation": {
        "name": "Basic Orbit Tutorial",
        "version": "1.0.0",
        "duration_minutes": 90
      },
      "monte_carlo": {
        "runs": 5,
        "sampling_method": "latin_hypercube",
        "seed": 42
      },
      "dispersions": {
        "semi_major_axis_m": {
          "distribution": { "type": "uniform", "min": 6950000.0, "max": 7050000.0 },
          "units": "m"
        },
        "eccentricity": {
          "distribution": { "type": "uniform", "min": 0.00005, "max": 0.002 },
          "units": "dimensionless"
        },
        "inclination_deg": {
          "distribution": { "type": "uniform", "min": 30.0, "max": 36.0 },
          "units": "deg"
        },
        "raan_deg": {
          "distribution": { "type": "uniform", "min": 40.0, "max": 55.0 },
          "units": "deg"
        },
        "arg_perigee_deg": {
          "distribution": { "type": "uniform", "min": 330.0, "max": 355.0 },
          "units": "deg"
        },
        "true_anomaly_deg": {
          "distribution": { "type": "uniform", "min": 70.0, "max": 100.0 },
          "units": "deg"
        },
        "duration_orbits": {
          "distribution": { "type": "uniform", "min": 0.70, "max": 0.80 },
          "units": "orbits"
        }
      },
      "compute": {
        "container": {
          "image": "tetryx/basilisk-basic-orbit:latest",
          "cpu_request": "500m",
          "memory_request": "1Gi"
        },
        "max_parallel_jobs": 2,
        "job_timeout_seconds": 1800
      },
      "early_termination": {
        "enabled": false,
        "check_frequency_hz": 1.0,
        "conditions": []
      },
      "telemetry": {
        "sampling_rate_hz": 0.1,
        "variables": [
          "orbit.radius_km",
          "orbit.speed_m_s",
          "orbit.semi_major_axis_km",
          "orbit.eccentricity",
          "orbit.inclination_deg",
          "orbit.true_anomaly_deg"
        ],
        "storage": {
          "s3_bucket": "your-telemetry-bucket",
          "s3_prefix": "basic-orbit/",
          "compression": "gzip"
        },
        "buffer_flush_interval_seconds": 30
      }
    }
  }' | jq
```

Response:

```json theme={null}
{
  "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "total_jobs": 5,
  "estimated_completion_time_mins": null
}
```

## Monitor progress

```bash theme={null}
# Poll until all jobs are terminal
curl -s https://api.lynx.tetryx.io/batches/3fa85f64-5717-4562-b3fc-2c963f66afa6 | jq '{
  status: .batch.status,
  progress: .jobs_by_status,
  throughput: .throughput_jobs_per_minute
}'
```

Example mid-run output:

```json theme={null}
{
  "status": "Running",
  "progress": {
    "Completed": 3,
    "Running": 1,
    "Pending": 1
  },
  "throughput": 4.2
}
```

## Retrieve results

Once the batch status is `Completed`, fetch all job results:

```bash theme={null}
curl -s https://api.lynx.tetryx.io/batches/3fa85f64-5717-4562-b3fc-2c963f66afa6/results | jq
```

Each job result looks like:

```json theme={null}
{
  "job_id": "a1b2c3d4-...",
  "success": true,
  "execution_time_secs": 34,
  "output_files": [
    "/tmp/tetryx-runs/a1b2c3d4-.../orbit_position.png",
    "/tmp/tetryx-runs/a1b2c3d4-.../summary.json",
    "/tmp/tetryx-runs/a1b2c3d4-.../trajectory.csv"
  ],
  "metadata": {
    "engine": "basilisk",
    "simulation_name": "Basic Orbit Tutorial",
    "entrypoint": "scenarios/aerospace/python/scenario_basic_orbit_lynx.py",
    "runner_id": "basilisk-runner-01"
  },
  "error_message": null
}
```

The `output_files` paths are on the runner's local filesystem. Artifact upload to S3 (and retrieval via the API) is a planned feature.

## Early termination

Early termination lets you kill a simulation mid-run when a condition is met, without waiting for the job timeout. The runner's watchdog monitors telemetry and sends SIGKILL to the subprocess if a condition fires.

To enable it in the batch config:

```json theme={null}
"early_termination": {
  "enabled": true,
  "check_frequency_hz": 10.0,
  "conditions": [
    {
      "variable": "orbit.radius_km",
      "condition": "< 6371",
      "description": "Orbit has decayed below Earth surface radius"
    }
  ]
}
```

The condition evaluates against the telemetry variables your scenario is emitting. If `orbit.radius_km` drops below 6371 km (Earth's mean radius), the runner kills the subprocess and marks the job failed.

## Adapting this scenario to your simulation

The integration contract your scenario must satisfy:

1. **Read** `TETRYX_RUN_CONFIG` at startup — this is the path to your sampled parameter set
2. **Write** artifacts to `TETRYX_OUTPUT_DIR` before exiting
3. **Exit 0** on success, **non-zero** on failure
4. **Optionally** stream scalar telemetry to `TETRYX_TELEMETRY_URL` during execution

That's the complete surface area. Your scenario can use any language, framework, or physics engine — as long as it respects those three required points, Lynx will orchestrate it.

## What comes next

* Connect a GitHub Actions workflow that submits a batch on push and fails CI if jobs fail
* Expand to 500+ runs with `"sampling_method": "latin_hypercube"` and a fixed seed for reproducible studies
* Add a second runner type (e.g. `px4_sitl`) using the same control plane and API
* Enable artifact upload to S3 for cross-machine artifact retrieval (planned)
