rftools
Python client for rftools.io — 241 RF & electronics calculators, plus 13 async simulation job types (FDTD, antenna NEC-2, filter Monte Carlo, and more), accessible from Python and the command line.
Installation
pip install rftools-io
Quick Start
Get a free API key (5 calls/month) without copying it anywhere:
rftools login
This prints a page and a code. Open the page, sign in or sign up, and approve the
code. The key is saved to ~/.config/rftools/credentials.json (%APPDATA%\rftools\
on Windows, readable by you only), and every later rftools.Client() and rftools
command reads it from there. The key is never printed. From Python, the same thing is
rftools.Client().login().
Or set a key you already have. The variable takes precedence over the saved file:
export RFTOOLS_API_KEY=rfc_your_key_here
import rftools
result = rftools.calculate("vswr-return-loss", {"vswr": 2.5})
print(result["returnLoss"]) # 9.54 dB
print(result.values) # {"returnLoss": 9.54, "reflectionCoeff": 0.333, ...}
Solving for an Input
Most calculators go forward: inputs to outputs. solve() goes the other way — given
every other input and a target value for one output, it finds the value of one input
that reaches it, in one metered call.
result = rftools.Client(api_key="rfc_live_xxx").solve(
"microstrip-impedance",
{"substrateHeight": 1.6, "dielectricConstant": 4.2, "copperThickness": 35},
"traceWidth", "impedance", 50.0,
grid=0.001, # round to a manufacturing grid; omit for the unrounded solution
)
print(result.value, result.reached) # 3.052 True
print(result.result["impedance"]) # the forward result at exactly that width
print(result.result.provenance.formula_ref)
grid and range (a (low, high) pair narrowing the search) are each sent only when
given. When no value in the search range reaches the target, the call still succeeds:
.reached is False and .value is the closest point found, with its own forward
.result. See Error Handling below — solve() raises the same typed
errors as calculate().
Authenticated Usage (API tier)
Get an API key at rftools.io/pricing ($19/mo, 10,000 calls/month).
import rftools
client = rftools.Client(api_key="rfc_live_xxx")
# or set environment variable: export RFTOOLS_API_KEY=rfc_live_xxx
result = client.calculate("free-space-path-loss", {"frequency": 2400, "distance": 100})
print(result["pathLoss"]) # dB
A key is sent as Authorization: Bearer <key> and as X-API-Key. The service accepts
either on every route.
Usage
The monthly allowance belongs to your account. Every key on the account draws on it.
usage = client.usage() # GET /v1/usage, which is never metered
print(usage.used, "of", usage.allowance, "used;", usage.remaining, "left")
print("resets", usage.reset_at)
for key in usage.keys:
print(key.key_id, key.label, key.used)
result = client.calculate("vswr-return-loss", {"vswr": 2.5})
print(result.usage.remaining) # from this response's X-Usage-* headers
print(client.last_usage) # the latest metered response's, refusals included
batch() results carry .usage too, and so does a job submitted with a key
(job.usage).
Provenance
A calculator result says how it was computed. provenance is None until the service
sends it.
p = result.provenance
print(p.method, p.version) # calculator:microstrip-impedance api@4ce2c2e0a1b2
print(p.formula_ref) # Hammerstad & Jensen (1980); Wadell, ...
for a in p.assumptions:
print(a.code, a.text)
print(p.valid_range.status) # inside | outside | unknown
print(p.valid_range.outside) # the inputs outside their stated bounds
BatchResultItem.provenance is the same per item. A member missing from an older
result reads as None and is listed in p.missing. The contract is vendored as
rftools/schemas/result-provenance.schema.json.
Batch Calculations
Run up to 50 calculations in a single HTTP request (API tier only):
results = client.batch([
("vswr-return-loss", {"vswr": 1.5}),
("vswr-return-loss", {"vswr": 2.0}),
("free-space-path-loss", {"frequency": 2400, "distance": 50}),
])
for r in results:
if r.ok:
print(r.values)
else:
print(f"Error: {r.error}")
Typed Category Stubs
IDE-friendly typed functions with parameter defaults matching the web UI:
from rftools.calculators import rf, pcb, antenna, power
# RF
result = rf.vswr_return_loss(vswr=2.5)
result = rf.free_space_path_loss(frequency=2400.0, distance=100.0)
# Antenna
result = antenna.dipole_antenna(frequency=433.0)
result = antenna.parabolic_dish_antenna(frequency=10000.0, diameter=0.6)
# PCB
result = pcb.trace_width_current(current=2.0, tempRise=10.0, thickness=1.0)
# Power
result = power.voltage_divider(vin=12.0, r1=10000.0, r2=10000.0)
All 13 categories available: rf, pcb, power, signal, antenna, general, motor, protocol, emc, thermal, sensor, unit_conversion, audio.
CLI
# Run a calculation
rftools calc vswr-return-loss --vswr 2.5
# With API key
RFTOOLS_API_KEY=rfc_xxx rftools calc free-space-path-loss --frequency 2400 --distance 100
# JSON output (pipe to jq)
rftools calc vswr-return-loss --vswr 2.5 --json | jq '.values.returnLoss'
# Solve one input for a target output (one metered call)
rftools solve microstrip-impedance --for traceWidth --target impedance=50 \
--grid 0.001 --param substrateHeight=1.6 --param dielectricConstant=4.2 \
--param copperThickness=35
# Narrow the search range instead of (or as well as) rounding to a grid
rftools solve microstrip-impedance --for traceWidth --target impedance=50 \
--range 0.1 10.0 --param substrateHeight=1.6
# Submit an async simulation job, wait for it, print the result summary
rftools sim eye_diagram --param dataRate=10e9 --file trace.s2p
# Submit and return immediately (poll later with the printed job id)
rftools sim fdtd_sparam --param solveMode=express --no-wait
# List all calculators
rftools list
# Filter by category
rftools list --category rf
# Show calculator inputs/outputs
rftools info free-space-path-loss
# Get an API key by approving a code on rftools.io (saved; never printed)
rftools login
rftools login --client-id my-tool --no-browser
# Library version
rftools version
Error Handling
Errors are classified by HTTP status code (and, for a finished job, by the service's
errorKind) — never by matching text in a message.
from rftools.exceptions import (
AuthError, QuotaError, RateLimitError, ValidationError, NotFound, JobFailed,
)
try:
result = client.calculate("vswr-return-loss", {"vswr": 2.5})
except QuotaError as e:
print(f"{e.used} of {e.limit} calls used; resets {e.reset_at}. Plans: {e.upgrade_url}")
except RateLimitError as e:
print(f"More than {e.limit} requests in {e.window_seconds}s. Retry after {e.retry_after}s")
except AuthError as e:
print(f"Invalid or missing API key. Get one: {e.key_url or 'rftools login'}")
except NotFound:
print("Unknown calculator slug")
except ValidationError as e:
print(f"Bad inputs: {e.failures}")
| Exception | When |
|---|---|
AuthError |
Missing, unknown or revoked API key (HTTP 401); .key_url is where to get one when the service names it. Also HTTP 403, and an upload attempted with no key, which is raised locally. A spent allowance is never an AuthError |
QuotaError |
The account's monthly allowance is spent (HTTP 402, on every metered route). Carries .reason, .limit, .used, .reset_at, .upgrade_url, .overage_url, .retry_after |
RateLimitError |
Too many requests too fast (HTTP 429). Carries .limit, .window_seconds, .retry_after |
LoginError |
login() got no key; .error is access_denied or expired_token |
ValidationError |
Bad inputs, locally or on the service (HTTP 400/422); .failures is the detail list |
NotFound |
Unknown calculator slug or job id (HTTP 404) |
JobFailed |
A job reached a terminal failed state; .kind is the service's errorKind |
APIError |
Unexpected/unclassified HTTP error, or a network failure |
RftoolsError is the base class for all of the above. Every exception raised for an HTTP
response carries .status_code and .error_kind (the service's errorKind). Pre-0.2
names (RFToolsError, NotFoundError) still work as aliases.
Before the API's metering release, a spent allowance on POST /v1/calculate came back
as HTTP 401. It is now 402 on every metered route, so it raises QuotaError.
Simulation jobs
Async simulation job types — FDTD transmission-line, antenna NEC-2, filter Monte Carlo,
eye diagram, and 9 more — run through the same Client. Parameters and input files are
validated locally against the job type's contract before anything is uploaded or sent.
import rftools
client = rftools.Client(api_key="rfc_live_xxx")
# Submit and wait for it to finish, then fetch a summary of the result
job = client.submit_job(
"eye_diagram",
{"dataRate": 10e9, "prbs": "prbs15"},
files=["trace.s2p"], # paths, or (filename, bytes) tuples
wait=True,
on_progress=lambda j: print(j.status, j.progress, j.stage),
)
print(job.result()) # summary: headline values, warnings, provenance
print(job.result(full=True)) # the whole result payload
# Or submit now, poll later
job = client.submit_job("fdtd_sparam", {"solveMode": "express"})
print(job.id, job.status)
...
job = client.get_job(job.id)
job = job.wait(timeout=60) # returns even if not finished by the deadline
A job that ends in failed raises JobFailed from wait() (or result()), carrying
.kind (one of the service's errorKind values) and the error message.
wait() polls the way the web app does: the first check happens immediately (no fixed
initial delay), then every 2s for the first 30s, every 5s until 5 minutes, and every 15s
after that, until the job reaches completed or failed.
Uploads need a key
Uploading a file needs an API key; set RFTOOLS_API_KEY or run rftools login. The
service refuses an anonymous upload, so upload() and submit_job(files=...) raise
AuthError with that sentence locally — before the file is read and before any request
is made. A job type
that takes no file still runs without a key, on the free lane, as before.
The uploaded object is recorded against the key's account, and only that account may submit it: a key obtained from a log or a shared link is refused as though it did not exist.
Uploaded files are capped at 10 MB (the API's limit) — upload()/submit_job() raise
ValidationError locally if a file is over that, before any request is made.
Async Support
import asyncio
import rftools
async def main():
async with rftools.AsyncClient(api_key="rfc_live_xxx") as client:
result = await client.calculate("vswr-return-loss", {"vswr": 2.5})
print(result["returnLoss"])
asyncio.run(main())
Ideal for running many calculations concurrently in FastAPI or async scripts.
Browse the Catalog
# List all calculators
calcs = rftools.list_calculators()
print(f"{len(calcs)} calculators available")
# Filter by category
rf_calcs = rftools.list_calculators(category="rf")
for c in rf_calcs:
print(f"{c.slug}: {c.title}")
# Get a single calculator's metadata
info = rftools.get_calculator("vswr-return-loss")
print(info.inputs) # tuple of InputField
print(info.outputs) # tuple of OutputField
All Calculator Categories
The typed stub catalog (rftools/calculators/, rftools.list_calculators()) is
generated from a snapshot of the registry and is due for a refresh to the current
241; run python scripts/generate_stubs.py --frontend-dir /path/to/rfhub/frontend
to regenerate it. The counts below describe that snapshot.
| Category | Count | Example |
|---|---|---|
rf |
26 | vswr-return-loss, free-space-path-loss, rf-link-budget |
pcb |
13 | trace-width-current, via-calculator, microstrip-impedance |
power |
20 | voltage-divider, led-resistor, battery-life |
signal |
13 | filter-designer, op-amp-gain, pwm-duty-cycle |
antenna |
8 | dipole-antenna, eirp-calculator, parabolic-dish-antenna |
general |
21 | lc-resonance, ohms-law, rc-time-constant |
motor |
18 | brushless-dc-motor, stepper-motor, servo-motor |
protocol |
11 | uart-baud-rate, i2c-pullup, can-bus-bit-timing |
emc |
16 | emi-filter-lc, shielding-effectiveness, ground-loop |
thermal |
6 | thermal-resistance, heat-sink, junction-temperature |
sensor |
17 | thermistor-ntc, strain-gauge, hall-effect |
unit-conversion |
17 | dbm-watts, frequency-wavelength, temperature |
audio |
17 | speaker-crossover, amplifier-gain, room-acoustics |
Contributing
Regenerate stubs after calculator changes:
python scripts/generate_stubs.py --frontend-dir /path/to/rfhub/frontend
Then bump the version in pyproject.toml and publish a new release.
License
MIT — see LICENSE.
Release files for rftools-io 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rftools_io-0.4.0.tar.gz | 227.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rftools_io-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 446.9 kB
Release files / rftools_io-0.4.0.tar.gz
| Download URL | rftools_io-0.4.0.tar.gz |
|---|---|
| Size | 227.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
993b8d608d19300e60d089c739cd75cabc3b16927e1ee5f45d73394ce7f66902
|
|
BLAKE2b-256 checksum How to use checksums |
d56b0d15c93064b0e2f3e7b16266abecff730083cd0df096a110b82544c50686
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / rftools_io-0.4.0-py3-none-any.whl
| Download URL | rftools_io-0.4.0-py3-none-any.whl |
|---|---|
| Size | 219.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fb9f715d689a81c3aeba9ffa6708eb7ab11666fe9493f0b7d4905e8dd172898c
|
|
BLAKE2b-256 checksum How to use checksums |
6f35d82acea8edbbd1f7a4d164d8154f2525be22598c88ed377f9f87474c9190
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log