Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ minisky server [--reload] # REST API server
minisky console # interactive console against the API
```

The FastAPI app lives in `minisky/server.py`; `minisky server` is the CLI entry point
The FastAPI app lives in `packages/minisky/minisky/server.py`; `minisky server` is the CLI entry point
(`MINISKY_HOST`/`MINISKY_PORT` env vars, default `0.0.0.0:8000`).

## Architecture

Full details in `docs/architecture.md` — read it before making structural changes. The essentials:

**Runtime ownership.** [`MiniSky`][minisky.runtime.MiniSky] owns one simulator object graph: settings, simulation, traffic, runner, console, navigation, command stack, plugins, replaceables, areas, variable explorer, random generators, and streaming hub. Unlike `bluesky`, there is no package-level `traf`, `sim`, `scr`, `runner`, or `navdb`.
**Runtime ownership.** [`MiniSky`][minisky.runtime.MiniSky] owns one simulator object graph: config, simulation, traffic, runner, console, navigation, command stack, plugins, replaceables, areas, variable explorer, random generators, and streaming hub. Unlike `bluesky`, there is no package-level `traf`, `sim`, `scr`, `runner`, or `navdb`.

**Lifecycle.** Use `with MiniSky(settings)` for manually stepped synchronous work. Use `async with MiniSky(settings)` and `await runtime.run()` when running the async loop. The FastAPI lifespan owns its background runner task and awaits asynchronous cleanup.
**Lifecycle.** Use `with MiniSky()` for manually stepped synchronous work with the default user config, or pass `config=` explicitly. Use `async with MiniSky()` and `await runtime.run()` when running the async loop. The FastAPI lifespan owns its background runner task and awaits asynchronous cleanup.

**Simulation loop.** `sim.step()` runs, in order: stack processing → time advance (only in `OP` state) → plugin `preupdate` → `traf.update()` (autopilot/FMS, conflict detection+resolution, performance limits, wind, position integration) → plugin `update`. States: `INIT`, `OP`, `HOLD`, `END`. Drive it either by calling `sim.step()` manually (embedding) or via `runner.run()` (wall-clock paced; `runner.speed` and `runner.forward()`).

Expand All @@ -45,22 +45,22 @@ Full details in `docs/architecture.md` — read it before making structural chan

## The command stack (critical convention)

Every text command — scenario file, REST `stack/` endpoint, or console — goes through `minisky.stack`. The built-in command table is `minisky/stack/commands.py`; plugins add commands with the `@command` decorator.
Every text command — scenario file, REST `stack/` endpoint, or console — goes through `minisky.stack`. The built-in command table is `packages/minisky/minisky/stack/commands.py`; plugins add commands with the `@command` decorator.

**Stack command arguments are parsed at runtime from the parameter annotations.** `minisky/stack/argparser.py` inspects `param.annotation` when a command is registered:
- **`Annotated` aliases (preferred):** `minisky/stack/argparser.py` exports `Acid`, `Wpt`, `Alt`, `Spd`, `Vspd`, `Hdg`, `Time`, `Txt`, `String`, `OnOff`, `Lat`, `Lon` — e.g. `def selaltcmd(self, idx: int, alt: Alt, vspd: Vspd | None = None)`. These are real type hints (lint- and pyright-clean) carrying the parser key as `Annotated` metadata; unions with `None` are unwrapped.
- **Argument-spec strings in the command table** (`minisky/stack/commands.py`, e.g. `"callsign,alt,[vspd]"`) are plain data looked up in the `argparsers` dict and *override* function annotations.
**Stack command arguments are parsed at runtime from the parameter annotations.** `packages/minisky/minisky/stack/argparser.py` inspects `param.annotation` when a command is registered:
- **`Annotated` aliases (preferred):** `packages/minisky/minisky/stack/argparser.py` exports `Acid`, `Wpt`, `Alt`, `Spd`, `Vspd`, `Hdg`, `Time`, `Txt`, `String`, `OnOff`, `Lat`, `Lon` — e.g. `def selaltcmd(self, idx: int, alt: Alt, vspd: Vspd | None = None)`. These are real type hints (lint- and pyright-clean) carrying the parser key as `Annotated` metadata; unions with `None` are unwrapped.
- **Argument-spec strings in the command table** (`packages/minisky/minisky/stack/commands.py`, e.g. `"callsign,alt,[vspd]"`) are plain data looked up in the `argparsers` dict and *override* function annotations.
- **Legacy DSL strings** as annotations (`alt: "alt"`) still parse, but don't write new ones — use the `Annotated` aliases so linting works.
- A real `type` annotation gets wrapped in `Parser(type)` (called on the argument text — fine for `int`/`float`/`str`, wrong for `bool`; use `OnOff`).

`Command.callback` resolves signatures with `inspect.signature(func, eval_str=True)` (falling back to raw strings for legacy DSL annotations), so `from __future__ import annotations` is safe in command modules.

`E711`/`E712`/`E721` are ignored in `pyproject.toml` because numpy overrides `==`/`is` elementwise, so `arr == None` is intentional and *not* equivalent to `arr is None`.

`minisky/traffic/asas/__init__.py` has a deliberately non-alphabetical import block wrapped in `# isort: off/on` (resolution before mvp, since MVP subclasses ConflictResolution) — don't "fix" it.
`packages/minisky/minisky/traffic/asas/__init__.py` has a deliberately non-alphabetical import block wrapped in `# isort: off/on` (resolution before mvp, since MVP subclasses ConflictResolution) — don't "fix" it.

## Conventions

- Package/dependency management is **uv**. Prefix Python invocations with `uv run`.
- After adding or changing a stack command, regenerate `docs/reference/commands.md` with the gen script.
- `settings.toml` holds runtime config (ASAS protected-zone margins, plugin path, `enabled_plugins`).
- MiniSky config defaults live only on `MiniSkyConfig`. The CLI optionally reads `default_user_config_toml_path()`; `--config` selects another file explicitly. `[plugins.<id>]` tables enable and configure plugins.
6 changes: 3 additions & 3 deletions docs/api/core.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# `minisky.core`

Core infrastructure: settings loading and the per-aircraft array bookkeeping that keeps
Core infrastructure: configuration loading and the per-aircraft array bookkeeping that keeps
all state index-aligned across the simulator.

## TrafficArrays

::: minisky.core.trafficarrays

## Settings
## Configuration

::: minisky.core.settings
::: minisky.core.config

## Variable explorer

Expand Down
8 changes: 4 additions & 4 deletions docs/api/minisky.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `minisky`

The top-level package exposes the explicit runtime owner, validated settings,
default settings path, and simulation-state constants.
The top-level package exposes the explicit runtime owner, validated configuration,
default user config path helpers, and simulation-state constants.

## Runtime

Expand All @@ -11,6 +11,6 @@ default settings path, and simulation-state constants.

::: minisky.SimulationState

## Settings
## Configuration

::: minisky.MiniSkySettings
::: minisky.MiniSkyConfig
32 changes: 20 additions & 12 deletions docs/api/plugin.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
# `minisky.plugin`

Runtime-owned plugin discovery, loading, timed hooks, per-aircraft entities,
and command declarations. See the [plugin guide](../guides/plugins.md).
Runtime-local plugin declarations, loading, hooks, entities, replacements, and
lifespan capabilities. See the [plugin guide](../guides/plugins.md).

## Plugin management
## Declarations

::: minisky.plugin.plugin.PluginManager
::: minisky.plugin.plugin.Plugin

## Plugin records
::: minisky.plugin.plugin.PluginContext

::: minisky.plugin.plugin.Plugin
::: minisky.plugin.plugin.PluginSpec

## Entity
## Runtime capabilities

::: minisky.plugin.entity.Entity
::: minisky.plugin.plugin.PluginRuntime

::: minisky.plugin.plugin.PluginStatus

## Timed hooks
## Management

::: minisky.plugin.timedfunction.TimedFunctionManager
::: minisky.plugin.plugin.PluginManager

## Entity

::: minisky.plugin.timedfunction.Timer
::: minisky.plugin.entity.Entity

## Stack command declarations
## Decorators

::: minisky.plugin.plugin_decorators.command

::: minisky.plugin.plugin_decorators.hook

::: minisky.plugin.plugin_decorators.replacement
32 changes: 19 additions & 13 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Architecture

MiniSky keeps BlueSky's core simulation model but removes the GUI, networking,
and node management. The mutabl state is owned by one [`MiniSky`][minisky.MiniSky] runtime.
and node management. The mutabl state is owned by a [`MiniSky`][minisky.MiniSky] runtime.

## Runtime ownership

Expand All @@ -15,18 +15,24 @@ Constructing `MiniSky` creates an independent object graph:
| [`runtime.console`][minisky.simulation.console.ConsoleIO] | [`ConsoleIO`][minisky.simulation.console.ConsoleIO] | Buffered text output |
| [`runtime.navigation`][minisky.tools.navdata.Navdatabase] | [`Navdatabase`][minisky.tools.navdata.Navdatabase] | Waypoints, airports, and airways |
| [`runtime.commands`][minisky.stack.CommandStack] | [`CommandStack`][minisky.stack.CommandStack] | Command registry, queue, and scenario state |
| [`runtime.plugins`][minisky.plugin.plugin.PluginManager] | [`PluginManager`][minisky.plugin.plugin.PluginManager] | Plugin records, hooks, timers, and state |
| [`runtime.plugins`][minisky.plugin.plugin.PluginManager] | [`PluginManager`][minisky.plugin.plugin.PluginManager] | Plugin declarations, hooks, and lifespans |
| `runtime.areas` | [`AreaFilter`][minisky.tools.areafilter.AreaFilter] | Named geographic areas |
| `runtime.variables` | [`VariableExplorer`][minisky.core.varexplorer.VariableExplorer] | Runtime data inspection |
| `runtime.streaming` | `StreamHub` | Rate-capped snapshot fan-out |

```python
from minisky import DEFAULT_SETTINGS_FILE, MiniSky, MiniSkySettings
import asyncio

settings = MiniSkySettings.from_file(DEFAULT_SETTINGS_FILE)
with MiniSky(settings) as runtime:
runtime.load_plugins()
runtime.simulation.step()
from minisky import MiniSky


async def main() -> None:
async with MiniSky() as runtime:
await runtime.plugins.load_configured()
runtime.simulation.step()


asyncio.run(main())
```

## The simulation loop
Expand All @@ -39,11 +45,11 @@ call to [`Simulation.step`][minisky.simulation.simulation.Simulation.step] does,
([`CommandStack.process`][minisky.stack.CommandStack.process]).
3. [`runtime.simulation.simt`][minisky.simulation.simulation.Simulation] and the simulated UTC clock move forward by `simdt`
(only in the `OP` state).
4. Timed plugin functions registered with the `preupdate` hook.
4. Plugin callbacks registered for the `preupdate` phase.
5. [`Traffic.update`][minisky.traffic.traffic.Traffic.update]
integrates aircraft state: autopilot/FMS logic, conflict detection and resolution,
aircraft performance limits, wind, and finally position integration.
6. Timed plugin functions registered with the `update` hook.
6. Plugin callbacks registered for the `update` phase.
7. The runtime-owned hub publishes when subscribers are present.

The simulation state machine uses [`SimulationState`][minisky.simulation.simulation.SimulationState]:
Expand Down Expand Up @@ -75,8 +81,8 @@ Classes that hold per-aircraft data derive from it and register their arrays:

```python
class Example(Entity):
def __init__(self, traffic):
super().__init__(traffic)
def __init__(self):
super().__init__()
with self.settrafarrays():
self.npassengers = np.array([])
```
Expand All @@ -97,7 +103,7 @@ or the console — goes through the same interpreter: [`minisky.stack`](api/stac
[`minisky.stack.argparser`](api/stack.md#argument-parsing), which knows aviation types
(`alt` accepts `FL100`, ft, or m; `spd` accepts CAS knots or Mach; `latlon` resolves
navaid names to coordinates).
- The built-in command table lives in `minisky/stack/commands.py`; plugins add commands
- The built-in command table lives in `packages/minisky/minisky/stack/commands.py`; plugins add commands
with the [`@command`][minisky.plugin.plugin_decorators.command] decorator.
- Scenario files (`.scn`) are simply time-stamped stack commands; `IC filename` loads one.

Expand All @@ -112,7 +118,7 @@ subsystems that act on it each timestep:
LNAV/VNAV logic following a [`Route`][minisky.traffic.route.Route] of waypoints.
- **Conflict detection** (`traffic/asas/detection.py`) — pairwise state-based detection
within a lookahead time against a protected zone (default 5 NM / 1000 ft, configurable
in `settings.toml`). Candidate pairs are pre-selected with a KD-tree on projected
in the [config file](guides/configuration.md)). Candidate pairs are pre-selected with a KD-tree on projected
positions plus a vertical reachability filter, so cost scales with local traffic
density rather than N².
- **Conflict resolution** (`traffic/asas/mvp.py`) — Modified Voltage Potential resolution
Expand Down
27 changes: 8 additions & 19 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ cd minisky
uv sync
```

## Configuration

MiniSky runs with the defaults defined by [`MiniSkyConfig`][minisky.MiniSkyConfig]. You only need a `config.toml` when you want to override a value or load a plugin automatically.

See [Configuration](guides/configuration.md) for the platform-specific default path and `--config` overrides.

## Your first simulation

Run one of the bundled scenarios to completion:
Expand Down Expand Up @@ -60,10 +66,9 @@ See the [command-line interface](guides/cli.md), [REST API](guides/rest-api.md),
## From Python

```python
from minisky import DEFAULT_SETTINGS_FILE, MiniSky, MiniSkySettings
from minisky import MiniSky

settings = MiniSkySettings.from_file(DEFAULT_SETTINGS_FILE)
with MiniSky(settings) as runtime:
with MiniSky() as runtime:
runtime.simulation.reset()
runtime.traffic.cre(
"KL315", lat=52.0, lon=4.0, hdg=45, alt=5000, spd=250
Expand All @@ -83,22 +88,6 @@ with MiniSky(settings) as runtime:
See the [Python library guide](guides/python-api.md) for details on runtime
ownership and stepping the simulation yourself.

## Configuration

Runtime settings live in `settings.toml` at the repository root, e.g. conflict-detection
lookahead time and protected-zone sizes, plus the current plugin search directory and startup list:

```toml
asas_dtlookahead = 300
asas_pzr = 5
asas_pzh = 1000
asas_marh = 1.05
asas_marv = 1.05

plugin_path = "example_plugins"
enabled_plugins = []
```

## Running the tests

```bash
Expand Down
17 changes: 5 additions & 12 deletions docs/guides/cli.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Command-line interface

MiniSky installs one top-level command, `minisky`, with subcommands for running
MiniSky installs a top-level command, `minisky`, with subcommands for running
scenarios, serving the API, using the console, and streaming snapshots.

```bash
Expand All @@ -11,18 +11,11 @@ uv run minisky --help

| Command | Purpose |
| --- | --- |
| `minisky run --scenario FILE [--speed N]` | Run a scenario file without interaction. |
| `minisky server [--host HOST] [--port PORT] [--reload]` | Start the REST and WebSocket API server. |
| `minisky run --scenario FILE [--speed N] [--config FILE]` | Run a scenario file without interaction. |
| `minisky server [--host HOST] [--port PORT] [--reload] [--config FILE]` | Start the REST and WebSocket API server. |
| `minisky console [--server URL] [--port PORT]` | Open an interactive console against a running server. |
| `minisky stream [--url URL] [--raw]` | Print snapshots from the `/stream` WebSocket. |

## Developer commands
## Config file

| Command | Purpose |
| --- | --- |
| `just check` | Run repository linting and type checks. |
| `just test` | Run the default test suite. |
| `just test-unit` | Run fast unit tests. |
| `just test-api` | Run opt-in REST API tests. |
| `just docs-serve` | Serve this documentation site locally. |
| `just docs-build` | Build the documentation site into `site/`. |
`run` and `server` use built-in defaults unless the platform-specific [default config file](configuration.md) exists. Pass `--config FILE` to choose another file explicitly.
77 changes: 77 additions & 0 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Configuration

MiniSky comes with reasonable runtime defaults, so you do not need a config file to start a simulation. To learn more about the shape and defaults, read the [`MiniSkyConfig` API][minisky.MiniSkyConfig] reference.

Create a config file only when you want to override those values or load plugins automatically.

## Default location

The command-line tools look for an optional `config.toml` in MiniSky's platform-specific user config directory:

| System | Default path |
| --- | --- |
| Linux | `$XDG_CONFIG_HOME/minisky/config.toml`, or `~/.config/minisky/config.toml` when `XDG_CONFIG_HOME` is not set |
| macOS | `$XDG_CONFIG_HOME/minisky/config.toml`, or `~/Library/Application Support/minisky/config.toml` when `XDG_CONFIG_HOME` is not set |
| Windows | `%LOCALAPPDATA%\minisky\config.toml` |

Ask the installed package for the exact path on your machine:

```bash
uv run python -c "from minisky import default_user_config_toml_path; print(default_user_config_toml_path())"
```

`default_user_config_dir()` and `default_user_config_toml_path()` describe the CLI convention. They return defaults, not mandatory locations.

## Create your config

Create the directory and an empty file when you are ready to customise MiniSky:

```bash
config_path="$(uv run python -c 'from minisky import default_user_config_toml_path; print(default_user_config_toml_path())')"
mkdir -p "$(dirname "$config_path")"
touch "$config_path"
```

## Choose another file

Pass `--config` when a command should use a different TOML file:

```bash
uv run minisky run --scenario scenarios/kl204.scn --config ./experiment.toml
uv run minisky server --config ./server.toml
```

## Use config from Python

```python
from minisky import MiniSky, MiniSkyConfig

# try to load from default user path, and if it doesn't exist use defauls
with MiniSky() as runtime:
...

# or explicitly pass a config
with MiniSky(config=MiniSkyConfig.from_path("experiment.toml")) as runtime:
...

# forcefully use built-in defaults
with MiniSky(config=MiniSkyConfig()) as runtime:
...
```

## Configure plugins

A table under `[plugins.<id>]` supplies that plugin's config and asks `load_configured()` to load it during startup:

```toml
[plugins.example]
interval = 2.0
```

Remove the table when you do not want that plugin loaded. An empty table is enough for a plugin that uses only its own defaults:

```toml
[plugins.example]
```

Plugin config is validated by the plugin's `config_class`.
Loading
Loading