simantic 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. simantic-0.2.0/.github/workflows/ci.yml +93 -0
  2. simantic-0.2.0/.gitignore +6 -0
  3. simantic-0.2.0/LICENSE +21 -0
  4. simantic-0.2.0/PKG-INFO +219 -0
  5. simantic-0.2.0/PUBLISHING.md +98 -0
  6. simantic-0.2.0/README.md +197 -0
  7. simantic-0.2.0/docs/session-api.md +108 -0
  8. simantic-0.2.0/examples/parallel_sweep.py +24 -0
  9. simantic-0.2.0/examples/step_and_peek.py +12 -0
  10. simantic-0.2.0/pyproject.toml +45 -0
  11. simantic-0.2.0/src/simantic/__init__.py +85 -0
  12. simantic-0.2.0/src/simantic/__main__.py +10 -0
  13. simantic-0.2.0/src/simantic/_cli.py +139 -0
  14. simantic-0.2.0/src/simantic/_locate.py +76 -0
  15. simantic-0.2.0/src/simantic/agent.py +175 -0
  16. simantic-0.2.0/src/simantic/analog.py +94 -0
  17. simantic-0.2.0/src/simantic/auth.py +221 -0
  18. simantic-0.2.0/src/simantic/engine.py +91 -0
  19. simantic-0.2.0/src/simantic/fixtures.py +144 -0
  20. simantic-0.2.0/src/simantic/install.py +313 -0
  21. simantic-0.2.0/src/simantic/mcu.py +163 -0
  22. simantic-0.2.0/src/simantic/pyrite.py +72 -0
  23. simantic-0.2.0/src/simantic/pytest_plugin.py +232 -0
  24. simantic-0.2.0/src/simantic/report.py +194 -0
  25. simantic-0.2.0/src/simantic/session.py +409 -0
  26. simantic-0.2.0/src/simantic/telemetry.py +225 -0
  27. simantic-0.2.0/tests/conftest.py +6 -0
  28. simantic-0.2.0/tests/fixtures/divider/divider.sim.toml +29 -0
  29. simantic-0.2.0/tests/test_agent.py +108 -0
  30. simantic-0.2.0/tests/test_auth.py +246 -0
  31. simantic-0.2.0/tests/test_fixtures.py +141 -0
  32. simantic-0.2.0/tests/test_install.py +410 -0
  33. simantic-0.2.0/tests/test_packaging.py +50 -0
  34. simantic-0.2.0/tests/test_plan.py +52 -0
  35. simantic-0.2.0/tests/test_pyrite.py +81 -0
  36. simantic-0.2.0/tests/test_report.py +164 -0
  37. simantic-0.2.0/tests/test_session.py +56 -0
  38. simantic-0.2.0/tests/test_spool.py +148 -0
  39. simantic-0.2.0/tests/test_telemetry.py +111 -0
@@ -0,0 +1,93 @@
1
+ # Tests every push and PR, and publishes to PyPI when a v* tag is pushed.
2
+ #
3
+ # Publishing uses PyPI Trusted Publishing (OIDC): the `pypi` environment
4
+ # exchanges this workflow's identity for a short-lived upload token, so there
5
+ # is no API token in repository secrets to leak or rotate. It must be
6
+ # configured once on PyPI before the first tagged release — see PUBLISHING.md.
7
+ #
8
+ # The SDK is pure Python and its tests need no simulator binary: the report
9
+ # fixtures are literal JSON in the shapes the schema allows, so CI stays
10
+ # independent of the engines this package drives.
11
+
12
+ name: ci
13
+
14
+ # DOCS ARE INERT. Prose-only commits compile nothing here. Safe only while main
15
+ # carries no required status checks — a path-filtered-away required check sits
16
+ # `Pending` forever and blocks the merge.
17
+ # (Actions does not support YAML anchors, hence the duplication.)
18
+ on:
19
+ push:
20
+ branches: [main]
21
+ tags: ["v*"]
22
+ paths-ignore:
23
+ - 'docs/**'
24
+ - '**.md'
25
+ - '.claude/**'
26
+ - 'LICENSE'
27
+ pull_request:
28
+ # `ready_for_review` is NOT a default activity type. Without it, flipping a
29
+ # draft to ready fires no event, so the macOS leg gated below never runs.
30
+ types: [opened, synchronize, reopened, ready_for_review]
31
+ paths-ignore:
32
+ - 'docs/**'
33
+ - '**.md'
34
+ - '.claude/**'
35
+ - 'LICENSE'
36
+
37
+ concurrency:
38
+ group: ${{ github.workflow }}-${{ github.ref }}
39
+ cancel-in-progress: true
40
+
41
+ jobs:
42
+ test:
43
+ runs-on: ${{ matrix.os }}
44
+ # The SDK is pure Python, so a macOS runner proves very little here that
45
+ # Linux does not — and it bills at $0.062/min against Linux's $0.006, with a
46
+ # 10x draw on the included-minutes allowance. Two of the six legs were
47
+ # macOS. It now joins on a push to main, on a v* tag (a release should be
48
+ # proven on all three), and on a pull request once marked ready for review.
49
+ # Windows is left unconditional: at 2x it is not what the bill is made of.
50
+ timeout-minutes: 15
51
+ strategy:
52
+ fail-fast: false
53
+ matrix:
54
+ os: ${{ (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
55
+ && fromJSON('["ubuntu-latest", "macos-latest"]')
56
+ || fromJSON('["ubuntu-latest"]') }}
57
+ # 3.11 is the floor (tomllib landed there); 3.13 guards the top end.
58
+ python: ["3.11", "3.13"]
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: astral-sh/setup-uv@v5
62
+ - run: uv run --python ${{ matrix.python }} --with pytest --with . pytest tests/ -q
63
+
64
+ build:
65
+ needs: test
66
+ runs-on: ubuntu-latest
67
+ timeout-minutes: 10
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - uses: astral-sh/setup-uv@v5
71
+ - run: uv build
72
+ # Catches the metadata problems PyPI rejects on upload — a malformed
73
+ # README, a bad classifier — while the tag can still be redone.
74
+ - run: uvx twine check dist/*
75
+ - uses: actions/upload-artifact@v4
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+
80
+ publish:
81
+ needs: build
82
+ if: startsWith(github.ref, 'refs/tags/v')
83
+ runs-on: ubuntu-latest
84
+ timeout-minutes: 10
85
+ environment: pypi
86
+ permissions:
87
+ id-token: write # required for trusted publishing; nothing else is
88
+ steps:
89
+ - uses: actions/download-artifact@v4
90
+ with:
91
+ name: dist
92
+ path: dist/
93
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ dist/
2
+ .venv/
3
+ .pytest_cache/
4
+ __pycache__/
5
+ *.egg-info/
6
+ uv.lock
simantic-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simantic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.5
2
+ Name: simantic
3
+ Version: 0.2.0
4
+ Summary: Python SDK and pytest plugin for the Simantic circuit and firmware simulators
5
+ Project-URL: Homepage, https://simantic.dev
6
+ Project-URL: Source, https://github.com/simantic-dev/simantic-py
7
+ Project-URL: Issues, https://github.com/simantic-dev/simantic-py/issues
8
+ Author: Simantic
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: circuit,eda,firmware,kicad,pytest,simulation,spice
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pythonnet>=3.0.5
20
+ Requires-Dist: pyyaml>=6
21
+ Description-Content-Type: text/markdown
22
+
23
+ # simantic
24
+
25
+ Python control of the [Simantic](https://simantic.dev) simulators — the
26
+ firmware engine hosted in your process, circuits via `analog-cli`. Everything
27
+ the CLIs can do, as objects and method calls: start a board or a multi-machine scenario, advance virtual
28
+ time by exact amounts, inject UART/GPIO/CAN/radio, read memory and RTOS state,
29
+ and run as many simulations in parallel as you have cores. pytest is one way
30
+ to use it, not a requirement.
31
+
32
+ > **Alpha — not stable.** Version 0.1.x. The API, the CLI surface, and the
33
+ > report schema may change without a deprecation period, and any release may
34
+ > break the previous one. Pin an exact version (`simantic==0.1.0`) if you
35
+ > depend on it. Not recommended for production pipelines yet.
36
+
37
+ ```bash
38
+ pip install simantic
39
+ ```
40
+
41
+ That is the whole setup for Python. The first `Sim(...)` fetches the
42
+ simulation engine (Simantic.Core plus a private .NET runtime — nothing else
43
+ to install) into `~/.simantic/engine/<version>/`, checksum-verified against
44
+ the public release manifest. `simantic install` fetches it up front, along
45
+ with the `sim` and `analog-cli` binaries if you also want the command-line
46
+ tools.
47
+
48
+ A Simantic account (`simantic auth`) is needed for one thing: resolving MCU
49
+ models by name (`mcu="STM32F401RE"`), which are fetched from your account
50
+ and cached in `~/.sim_cache`. A platform file you supply (`repl=`) needs no
51
+ account at all.
52
+
53
+ `simantic auth` opens a browser tab to sign in — like `gh auth login` — and
54
+ stores the resulting token in `~/.sim_id`, the same file the CLIs use, so one
55
+ login covers all of them. In a script or CI, pass `--token` or pipe one in
56
+ (`echo $TOKEN | simantic auth`) instead of opening a browser. Create a token
57
+ on the dashboard's `/account/api` page. `--no-browser` falls back to an
58
+ interactive prompt for a pasted token.
59
+
60
+ Every download — engine or binary — is verified against the checksum in the
61
+ release manifest. The package on PyPI contains only Python; the simulators
62
+ are never in the wheel.
63
+
64
+ Already have the binaries? Point `$SIMANTIC_ANALOG_CLI` and `$SIMANTIC_SIM`
65
+ at them, or put them on PATH — both take precedence over a managed install.
66
+ `simantic status` shows what is authenticated and which binary each name
67
+ resolves to.
68
+
69
+ ## Drive a simulation
70
+
71
+ A `Sim` is a live simulation you control. Time advances only when you ask, so
72
+ a script is deterministic and your think-time is free:
73
+
74
+ ```python
75
+ from simantic import Sim
76
+
77
+ with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
78
+ sim.expect("ready")
79
+ sim.inject_gpio("gpioc", 13, True) # press the user button
80
+ m = sim.expect("button pressed")
81
+ assert m.virtual_seconds < 0.010 # within 10 virtual ms
82
+ assert sim.read_u32("press_count") == 1
83
+ ```
84
+
85
+ The same class runs multi-machine scenarios with scripted peers
86
+ (`Sim(scenario={...})`). The engine lives in your process (one emulation per
87
+ process), so a parameter sweep is a `ProcessPoolExecutor` over plain
88
+ functions. See
89
+ [docs/session-api.md](docs/session-api.md) and `examples/`.
90
+
91
+ One-shot runs ("run 5 s, give me the transcript") are `run_firmware(...)`.
92
+
93
+ ## Using it from pytest (optional)
94
+
95
+ `Sim` needs no plugin — construct it inside any test. If you also keep
96
+ manifests, installing the package registers two collectors that turn them
97
+ into individually addressable pytest items:
98
+
99
+ - `*.sim.toml` — one item per `[[test]]` table (analog)
100
+ - `test.yaml` — one item per fixture (firmware)
101
+
102
+ ```console
103
+ $ pytest hardware/ firmware/
104
+ hardware/psu/psu.sim.toml::schematic-erc PASSED
105
+ hardware/psu/psu.sim.toml::rails-op PASSED
106
+ hardware/psu/psu.sim.toml::startup-settling FAILED
107
+ hardware/psu/psu.sim.toml::board-drc SKIPPED (no .kicad_pcb)
108
+ firmware/tests/gpio-loopback/test.yaml::gpio-loopback PASSED
109
+ ```
110
+
111
+ Because these are ordinary pytest items you get `-k` filtering, `--junitxml`
112
+ for CI, xdist parallelism, and per-test durations. Failures print the
113
+ runner's own explanation rather than a Python traceback:
114
+
115
+ ```
116
+ startup-settling (tran): fail
117
+ FAIL settle-time: V(OUT) measured 0.0082 (expected max 0.006, margin -0.0022)
118
+ ```
119
+
120
+ Tests that cannot run in the current environment skip rather than fail — a
121
+ missing binary, an unconfigured server, an analysis the installed CLI does
122
+ not support, a check inapplicable to the project. A red run means a
123
+ simulation ran and disagreed with its expectations.
124
+
125
+ ## Library
126
+
127
+ ### Circuits
128
+
129
+ ```python
130
+ import simantic
131
+
132
+ report = simantic.run_tests("hardware/psu")
133
+ print(f"{report.summary.passed}/{report.summary.total} passed")
134
+
135
+ for m in report.test("rails-op").measurements:
136
+ print(m.describe()) # out-dc: V(OUT) measured 1.597 (expected eq 1.597 +/- 0.02, margin 0.02)
137
+ ```
138
+
139
+ A failing test is data, not an exception: it arrives in the report with its
140
+ measured value, declared bounds, and margin. Only conditions that prevent a
141
+ run at all — bad project, missing `kicad-cli`, invalid testplan — raise
142
+ `AnalogCliError`.
143
+
144
+ ### Firmware
145
+
146
+ The shortest path is a pytest fixture — no manifest, no flags:
147
+
148
+ ```python
149
+ def test_firmware_boots(pyrite):
150
+ run = pyrite("build/zephyr.elf", board="stm32f401",
151
+ expect=["Hello World!"], expect_absent=["FAULT"])
152
+ assert run.passed, run.failure_report()
153
+ ```
154
+
155
+ `pyrite` runs the ELF offline on the pure-Rust backend and hands back the
156
+ UART transcript. The fixture skips when no binary is installed, so a suite
157
+ stays green on a machine that has not run `smtc install pyrite`.
158
+
159
+ The same runner is available as a plain function, and `sim` has its own:
160
+
161
+ ```python
162
+ run = simantic.run_firmware(
163
+ "build/zephyr.elf",
164
+ mcu="STM32F401RE", # resolved by the backend through your account
165
+ expect=["RESULT: PASS"],
166
+ expect_absent=["RESULT: FAIL"],
167
+ )
168
+ assert run.passed, run.failure_report()
169
+ ```
170
+
171
+ `sim` emits no structured report — the only observable is UART text — so the
172
+ verdict is substring matching, the same contract `test.yaml` manifests use.
173
+ Pass `repl=` instead of `mcu=` for a platform file you author yourself.
174
+
175
+ MCU models are not distributed with this package: `mcu=` resolves them
176
+ through your account. If you have a local model library, set
177
+ `$SIMANTIC_MCU_LIB` to resolve from it instead — which is also what applying
178
+ a fixture's `overlay` fragment requires.
179
+
180
+ Some installations need a separate simulation server. When one does, the SDK
181
+ raises `ServerNotConfigured` and the pytest plugin skips, rather than
182
+ reporting a firmware failure.
183
+
184
+ ## Telemetry
185
+
186
+ When you are authenticated, a completed pytest session reports its **shape**
187
+ to your account: how many simulator tests ran, how many passed, failed, or
188
+ skipped, plus this package's version, your Python version, OS, and CPU
189
+ architecture. One request per `pytest` invocation, never per test.
190
+
191
+ It also counts **which calls you make** — SDK functions, MCP tool names, and
192
+ `smtc` subcommands, by name only. These are buffered in
193
+ `~/.simantic/usage.jsonl` and uploaded as counts at most once an hour, so no
194
+ simulation ever waits on the network. You can read that file at any time; it
195
+ is one JSON object per line and contains nothing but call names.
196
+
197
+ It does **not** send file paths, project names, test names, firmware, or
198
+ simulation output. Those are yours.
199
+
200
+ ```bash
201
+ export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
202
+ ```
203
+
204
+ `smtc status` prints exactly what is sent and whether it is on. Reporting is
205
+ best-effort: if it fails, is blocked, or you are offline, your tests are
206
+ unaffected and nothing is printed.
207
+
208
+ ## Compatibility
209
+
210
+ Speaks the `analog-cli.test-report/1` schema. Additive fields within that
211
+ revision are tolerated; a breaking revision raises `ReportError` rather than
212
+ silently misreading a report.
213
+
214
+ Multi-machine `test.yaml` fixtures — those with a `machines:` map — need the
215
+ `--scenario` runner and are not driven yet; they report as skips.
216
+
217
+ ## License
218
+
219
+ MIT. The simulators it drives are separate software under their own terms.
@@ -0,0 +1,98 @@
1
+ # Publishing `simantic` to PyPI
2
+
3
+ First-time setup, then the per-release loop. Steps marked **[you]** need a
4
+ human with the accounts; everything else is automated in
5
+ `.github/workflows/python.yml`.
6
+
7
+ ## One-time setup
8
+
9
+ ### 1. Accounts **[you]**
10
+
11
+ Create accounts on both indexes, with 2FA (PyPI requires it for publishing):
12
+
13
+ - <https://test.pypi.org/account/register/> — the rehearsal index
14
+ - <https://pypi.org/account/register/> — the real one
15
+
16
+ TestPyPI is a genuinely separate site with separate credentials. Use it for
17
+ the first upload so a mistake is not permanent.
18
+
19
+ ### 2. Claim the name **[you]**
20
+
21
+ `simantic` was unregistered as of this writing. Names are first-come and a
22
+ published version number can never be reused or overwritten — only yanked —
23
+ so publish `0.1.0` to TestPyPI first, confirm it looks right, and only then
24
+ push the real tag. Claiming early is cheap insurance against someone else
25
+ taking it.
26
+
27
+ ### 3. Configure Trusted Publishing **[you]**
28
+
29
+ This replaces API tokens with short-lived OIDC credentials, so there is no
30
+ long-lived secret in the repo. On PyPI, go to your account's **Publishing**
31
+ page and add a *pending* publisher (pending = the project does not exist
32
+ yet, which is the case before the first upload):
33
+
34
+ | Field | Value |
35
+ |---|---|
36
+ | PyPI project name | `simantic` |
37
+ | Owner | `simantic-dev` |
38
+ | Repository name | `pippy` (the GitHub repo name, not the package name) |
39
+ | Workflow name | `ci.yml` |
40
+ | Environment name | `pypi` |
41
+
42
+ Then in GitHub: **Settings → Environments → New environment → `pypi`**. Add
43
+ required reviewers there if you want a human approval gate before any upload.
44
+
45
+ Repeat the whole step on TestPyPI if you want the rehearsal automated;
46
+ otherwise do the rehearsal upload by hand (below).
47
+
48
+ ## Rehearsal upload **[you]**
49
+
50
+ ```bash
51
+ uv build
52
+ uvx twine check dist/* # metadata PyPI would reject
53
+ uvx twine upload --repository testpypi dist/*
54
+ ```
55
+
56
+ Then confirm a clean machine can install and import it:
57
+
58
+ ```bash
59
+ uv run --with simantic --index https://test.pypi.org/simple/ \
60
+ --index-strategy unsafe-best-match \
61
+ python -c "import simantic; print(simantic.__version__)"
62
+ ```
63
+
64
+ ## Releasing
65
+
66
+ 1. Bump `version` in `pyproject.toml` **and** `__version__` in
67
+ `src/simantic/__init__.py`. They are asserted equal by the test suite.
68
+ 2. Merge to `main`.
69
+ 3. Tag and push:
70
+
71
+ ```bash
72
+ git tag v0.1.0
73
+ git push origin v0.1.0
74
+ ```
75
+
76
+ The workflow tests on Linux/macOS/Windows across 3.11 and 3.13, builds the
77
+ sdist and wheel, runs `twine check`, and uploads via trusted publishing.
78
+
79
+ ## Versioning
80
+
81
+ Semantic versioning on the SDK's own surface, which is independent of the
82
+ `analog-cli` version it drives. The coupling that matters is the report
83
+ schema: `simantic` speaks `analog-cli.test-report/1` and refuses anything
84
+ else, so a schema revision in the CLI is a major bump here.
85
+
86
+ Stay on `0.x` until the API has survived real use. Pre-1.0 signals that
87
+ breaking changes can still happen, which is honest for a first release.
88
+
89
+ ## Notes for later
90
+
91
+ - **Binary wheels.** The SDK deliberately does not bundle a simulator; it
92
+ resolves each binary from its `$SIMANTIC_*` variable, then a `_bin/`
93
+ directory inside the package, then PATH. A separate platform-specific wheel
94
+ can therefore drop a binary into `_bin/` and be found with no SDK change,
95
+ which is the seam to use if engines are ever distributed through PyPI.
96
+ - **Closed-source binaries are fine on PyPI.** Wheels need not contain
97
+ source, and the index has no open-source requirement. Keeping this SDK MIT
98
+ while the engines stay proprietary is a normal arrangement.
@@ -0,0 +1,197 @@
1
+ # simantic
2
+
3
+ Python control of the [Simantic](https://simantic.dev) simulators — the
4
+ firmware engine hosted in your process, circuits via `analog-cli`. Everything
5
+ the CLIs can do, as objects and method calls: start a board or a multi-machine scenario, advance virtual
6
+ time by exact amounts, inject UART/GPIO/CAN/radio, read memory and RTOS state,
7
+ and run as many simulations in parallel as you have cores. pytest is one way
8
+ to use it, not a requirement.
9
+
10
+ > **Alpha — not stable.** Version 0.1.x. The API, the CLI surface, and the
11
+ > report schema may change without a deprecation period, and any release may
12
+ > break the previous one. Pin an exact version (`simantic==0.1.0`) if you
13
+ > depend on it. Not recommended for production pipelines yet.
14
+
15
+ ```bash
16
+ pip install simantic
17
+ ```
18
+
19
+ That is the whole setup for Python. The first `Sim(...)` fetches the
20
+ simulation engine (Simantic.Core plus a private .NET runtime — nothing else
21
+ to install) into `~/.simantic/engine/<version>/`, checksum-verified against
22
+ the public release manifest. `simantic install` fetches it up front, along
23
+ with the `sim` and `analog-cli` binaries if you also want the command-line
24
+ tools.
25
+
26
+ A Simantic account (`simantic auth`) is needed for one thing: resolving MCU
27
+ models by name (`mcu="STM32F401RE"`), which are fetched from your account
28
+ and cached in `~/.sim_cache`. A platform file you supply (`repl=`) needs no
29
+ account at all.
30
+
31
+ `simantic auth` opens a browser tab to sign in — like `gh auth login` — and
32
+ stores the resulting token in `~/.sim_id`, the same file the CLIs use, so one
33
+ login covers all of them. In a script or CI, pass `--token` or pipe one in
34
+ (`echo $TOKEN | simantic auth`) instead of opening a browser. Create a token
35
+ on the dashboard's `/account/api` page. `--no-browser` falls back to an
36
+ interactive prompt for a pasted token.
37
+
38
+ Every download — engine or binary — is verified against the checksum in the
39
+ release manifest. The package on PyPI contains only Python; the simulators
40
+ are never in the wheel.
41
+
42
+ Already have the binaries? Point `$SIMANTIC_ANALOG_CLI` and `$SIMANTIC_SIM`
43
+ at them, or put them on PATH — both take precedence over a managed install.
44
+ `simantic status` shows what is authenticated and which binary each name
45
+ resolves to.
46
+
47
+ ## Drive a simulation
48
+
49
+ A `Sim` is a live simulation you control. Time advances only when you ask, so
50
+ a script is deterministic and your think-time is free:
51
+
52
+ ```python
53
+ from simantic import Sim
54
+
55
+ with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
56
+ sim.expect("ready")
57
+ sim.inject_gpio("gpioc", 13, True) # press the user button
58
+ m = sim.expect("button pressed")
59
+ assert m.virtual_seconds < 0.010 # within 10 virtual ms
60
+ assert sim.read_u32("press_count") == 1
61
+ ```
62
+
63
+ The same class runs multi-machine scenarios with scripted peers
64
+ (`Sim(scenario={...})`). The engine lives in your process (one emulation per
65
+ process), so a parameter sweep is a `ProcessPoolExecutor` over plain
66
+ functions. See
67
+ [docs/session-api.md](docs/session-api.md) and `examples/`.
68
+
69
+ One-shot runs ("run 5 s, give me the transcript") are `run_firmware(...)`.
70
+
71
+ ## Using it from pytest (optional)
72
+
73
+ `Sim` needs no plugin — construct it inside any test. If you also keep
74
+ manifests, installing the package registers two collectors that turn them
75
+ into individually addressable pytest items:
76
+
77
+ - `*.sim.toml` — one item per `[[test]]` table (analog)
78
+ - `test.yaml` — one item per fixture (firmware)
79
+
80
+ ```console
81
+ $ pytest hardware/ firmware/
82
+ hardware/psu/psu.sim.toml::schematic-erc PASSED
83
+ hardware/psu/psu.sim.toml::rails-op PASSED
84
+ hardware/psu/psu.sim.toml::startup-settling FAILED
85
+ hardware/psu/psu.sim.toml::board-drc SKIPPED (no .kicad_pcb)
86
+ firmware/tests/gpio-loopback/test.yaml::gpio-loopback PASSED
87
+ ```
88
+
89
+ Because these are ordinary pytest items you get `-k` filtering, `--junitxml`
90
+ for CI, xdist parallelism, and per-test durations. Failures print the
91
+ runner's own explanation rather than a Python traceback:
92
+
93
+ ```
94
+ startup-settling (tran): fail
95
+ FAIL settle-time: V(OUT) measured 0.0082 (expected max 0.006, margin -0.0022)
96
+ ```
97
+
98
+ Tests that cannot run in the current environment skip rather than fail — a
99
+ missing binary, an unconfigured server, an analysis the installed CLI does
100
+ not support, a check inapplicable to the project. A red run means a
101
+ simulation ran and disagreed with its expectations.
102
+
103
+ ## Library
104
+
105
+ ### Circuits
106
+
107
+ ```python
108
+ import simantic
109
+
110
+ report = simantic.run_tests("hardware/psu")
111
+ print(f"{report.summary.passed}/{report.summary.total} passed")
112
+
113
+ for m in report.test("rails-op").measurements:
114
+ print(m.describe()) # out-dc: V(OUT) measured 1.597 (expected eq 1.597 +/- 0.02, margin 0.02)
115
+ ```
116
+
117
+ A failing test is data, not an exception: it arrives in the report with its
118
+ measured value, declared bounds, and margin. Only conditions that prevent a
119
+ run at all — bad project, missing `kicad-cli`, invalid testplan — raise
120
+ `AnalogCliError`.
121
+
122
+ ### Firmware
123
+
124
+ The shortest path is a pytest fixture — no manifest, no flags:
125
+
126
+ ```python
127
+ def test_firmware_boots(pyrite):
128
+ run = pyrite("build/zephyr.elf", board="stm32f401",
129
+ expect=["Hello World!"], expect_absent=["FAULT"])
130
+ assert run.passed, run.failure_report()
131
+ ```
132
+
133
+ `pyrite` runs the ELF offline on the pure-Rust backend and hands back the
134
+ UART transcript. The fixture skips when no binary is installed, so a suite
135
+ stays green on a machine that has not run `smtc install pyrite`.
136
+
137
+ The same runner is available as a plain function, and `sim` has its own:
138
+
139
+ ```python
140
+ run = simantic.run_firmware(
141
+ "build/zephyr.elf",
142
+ mcu="STM32F401RE", # resolved by the backend through your account
143
+ expect=["RESULT: PASS"],
144
+ expect_absent=["RESULT: FAIL"],
145
+ )
146
+ assert run.passed, run.failure_report()
147
+ ```
148
+
149
+ `sim` emits no structured report — the only observable is UART text — so the
150
+ verdict is substring matching, the same contract `test.yaml` manifests use.
151
+ Pass `repl=` instead of `mcu=` for a platform file you author yourself.
152
+
153
+ MCU models are not distributed with this package: `mcu=` resolves them
154
+ through your account. If you have a local model library, set
155
+ `$SIMANTIC_MCU_LIB` to resolve from it instead — which is also what applying
156
+ a fixture's `overlay` fragment requires.
157
+
158
+ Some installations need a separate simulation server. When one does, the SDK
159
+ raises `ServerNotConfigured` and the pytest plugin skips, rather than
160
+ reporting a firmware failure.
161
+
162
+ ## Telemetry
163
+
164
+ When you are authenticated, a completed pytest session reports its **shape**
165
+ to your account: how many simulator tests ran, how many passed, failed, or
166
+ skipped, plus this package's version, your Python version, OS, and CPU
167
+ architecture. One request per `pytest` invocation, never per test.
168
+
169
+ It also counts **which calls you make** — SDK functions, MCP tool names, and
170
+ `smtc` subcommands, by name only. These are buffered in
171
+ `~/.simantic/usage.jsonl` and uploaded as counts at most once an hour, so no
172
+ simulation ever waits on the network. You can read that file at any time; it
173
+ is one JSON object per line and contains nothing but call names.
174
+
175
+ It does **not** send file paths, project names, test names, firmware, or
176
+ simulation output. Those are yours.
177
+
178
+ ```bash
179
+ export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
180
+ ```
181
+
182
+ `smtc status` prints exactly what is sent and whether it is on. Reporting is
183
+ best-effort: if it fails, is blocked, or you are offline, your tests are
184
+ unaffected and nothing is printed.
185
+
186
+ ## Compatibility
187
+
188
+ Speaks the `analog-cli.test-report/1` schema. Additive fields within that
189
+ revision are tolerated; a breaking revision raises `ReportError` rather than
190
+ silently misreading a report.
191
+
192
+ Multi-machine `test.yaml` fixtures — those with a `machines:` map — need the
193
+ `--scenario` runner and are not driven yet; they report as skips.
194
+
195
+ ## License
196
+
197
+ MIT. The simulators it drives are separate software under their own terms.