Command-line interface
RayRF Studio ships with a headless command-line interface. The same executable that hosts the desktop UI also exposes a CLI surface for batch runs, scripted parameter sweeps, and remote validation harnesses. The CLI uses the same simulation engine, the same project format, and the same license cache as the UI, without the windowed front end.
auth subcommand is the one exception: it does not need the CLI capability, so a fresh install can activate a key from the command line and then move on to run. Hobby plans can still use the desktop app, but cannot run rayrf run.Versioning
The CLI surface is versioned so future changes can ship as a new flag instead of breaking existing scripts. The only available version is v1. Pin to it explicitly with the --cli-v1 flag in front of the subcommand. The bare --cli flag and a bare positional subcommand (run, reprocess, auth) route through the latest stable version, currently v1. When a future v2 lands, the v1 surface stays available so a script that pins--cli-v1 keeps working.
Invocation
The CLI is invoked through the same executable that runs the desktop app. The first argument picks the CLI surface. Everything after it is parsed by the CLI itself.
# Windows
rayrf.exe --cli-v1 auth --license-key XXXX-XXXX-XXXX-XXXX
rayrf.exe --cli-v1 run --project foo.rfsim --quality high --save-after-run
rayrf.exe --cli-v1 reprocess --project foo.rfsim --fft-mult 4
# Linux: invoke the AppImage directly with the same flags.
./RayRF-Studio-x86_64.AppImage --cli-v1 auth --license-key XXXX-XXXX-XXXX-XXXX
./RayRF-Studio-x86_64.AppImage --cli-v1 run --project foo.rfsim --quality high --save-after-run
# Aliases kept for back-compat. Both route to v1 today:
rayrf.exe --cli run --project foo.rfsim
rayrf.exe run --project foo.rfsimOn Windows the executable lives next to the installed app (default path C:\Program Files\RayRF\rayrf.exe). Add the install directory to your PATH if you want to call rayrf without the full path.
On Linux the AppImage is self-contained and runs from wherever you put it. To shorten the invocation, either symlink it into a directory on your PATH (ln -s /opt/rayrf/RayRF-Studio-x86_64.AppImage ~/.local/bin/rayrf), or extract it once with ./RayRF-Studio-x86_64.AppImage --appimage-extract and call ./squashfs-root/RayRF directly. The examples below use the Windows form for brevity. Substitute the AppImage path or your symlink on Linux.
Authentication
The auth subcommand binds a license key to this PC and persists the result in the same QSettings location the desktop UI uses. After a successful activation, both the UI and the CLI recognize the license on launch with no further prompts. Three actions are available:
rayrf.exe --cli-v1 auth --license-key XXXX-XXXX-XXXX-XXXX
rayrf.exe --cli-v1 auth --status
rayrf.exe --cli-v1 auth --deactivateActivate
Activation does three things in order: it computes a stable machine fingerprint from the OS (the same value the C++ backend uses to verify the license token), POSTs the (key, machine id) pair to /api/licenses/activate on rayrf.com, then pulls a fresh /api/app/status so the local cache learns the plan tier, the founding flag, and the grace window. The cache is written to QSettings under the RayRF organisation / RayRF-Frontend application keys. Nothing is written to stdout that you would not want a script log to capture.
The license key is case-insensitive on input and stored upper-case. Leading and trailing whitespace are stripped, so pasting a key from an email client does the right thing.
> rayrf.exe --cli-v1 auth --license-key aaaa-bbbb-cccc-dddd
Activating license on machine 8F2A....2E1D ...
Activated.
License key: AAAA-BBBB-CCCC-DDDD
Plan: RayRF Pro
Account: you@example.com
License type: pro
Machine id: 8F2A....2E1D
Last check: 2026-05-13T09:42:11Status
auth --status (also the default action when no other flag is given) prints the cached state and then runs a fresh server check. The server reply is what the periodic background poller in the UI sees, so a status call from the CLI is the same diagnostic you would get by clicking Refresh plan status in the desktop account menu.
Deactivate
auth --deactivate releases this machine's seat on the server and clears the local QSettings entries. If the server cannot be reached, the local cache is still cleared so the user can sign in elsewhere. The server side reconciles on the next periodic sweep.
HKEY_CURRENT_USER\Software\RayRF\RayRF-Frontend. On Linux it is the QSettings INI file at ~/.config/RayRF/RayRF-Frontend.conf. Both stores hold the license key, license type, account email, the latest plan snapshot, the timestamp of the last successful status check, and the offline grace window expiry. Wiping the store is equivalent to a clean auth --deactivate on the local side. The server-side seat stays bound until the next reconcile.Machine binding and offline grace
A license seat is bound to a specific machine fingerprint. The fingerprint is derived from a stable OS source (Windows MachineGuid under HKLM\SOFTWARE\Microsoft\Cryptography, or Linux /etc/machine-id with a fallback to /var/lib/dbus/machine-id) so it survives across reboots and reinstalls but changes when you move to a new machine. To run on a different PC, deactivate from the old one first or release the seat from the account dashboard.
After a successful status check the local cache trusts the license for a 7 day offline grace window. Inside that window, the CLI works without an internet connection. Once the window lapses without a fresh check, the app degrades to viewer mode and rayrf run exits with code 2 until a check succeeds again.
Run subcommand
rayrf run loads a single project or every project in a folder, exports the simulator inputs, runs the FDTD backend, and writes standard PNG and CSV outputs alongside an optional save of the updated project file. Two source modes are mutually exclusive and one is required:
rayrf.exe --cli-v1 run --project path/to/case.rfsim
rayrf.exe --cli-v1 run --folder path/to/cases --recursiveThe folder mode walks .rfsim and .emflux files in alphabetical order and runs them sequentially. Each project runs against a fresh backend process so a crash in one case does not interrupt the rest of the batch.
Backend autodetection
By default the CLI looks for the GPU backend executable next to the frontend and in the development build tree. The lookup order is the same directory as the frontend, looking for rayrf.exe / rayrf / backend.exe / backend in that order, then RayRF-Core/build/Release/rayrf(.exe) relative to the install root. On a shipping install this resolves to the bundled backend.exe (Windows) or backend (inside the extracted AppImage on Linux). Override with --backend PATH if your backend binary lives somewhere else (useful for swapping in a development build without reinstalling).
Quality, sim mode, convergence
Auto mode picks mesh size, PML thickness, air margins, ringdown criterion, and convergence cadence from a single quality knob. The quality knob accepts a name, a slider fraction in [0, 1], or a decimal in [0, 3]:
--quality low | medium | high | very-high
--quality low-med | med-high | high-vhigh # midpoints
--quality 1.7 # direct decimal in [0, 3]
--quality-frac 0.72 # slider fraction, mapped to [0, 3]
--sim-mode auto | advanced # force the mode
--convergence | --no-convergence
--use-center-freq-for-margins | --no-use-center-freq-for-margins--sim-mode advanced turns off auto mode entirely. The numeric override flags below take precedence. --convergence and --no-convergence override the project's saved convergence-study flag.
Numeric overrides
These apply on top of the project file. Each one is optional. An omitted flag uses whatever the project already had.
--air-around MM # FDTD air margin around the geometry
--air-above MM # FDTD air above
--air-below MM # FDTD air below
--dx MM # Yee cell X
--dy MM # Yee cell Y
--dz MM # Yee cell Z
--time-steps N
--freq-min MHZ
--freq-max MHZ
--boundary-condition PML | PEC # applied to all six faces
--eps-r VALUE # applied to every dielectric layer
--thickness MM # applied to every dielectric layer
--loss-tangent VALUE # applied to every dielectric layer
--fft-length-multiplier N # zero-padding for S-parameter FFT, >= 1--eps-r, --thickness, --loss-tangent) apply to every dielectric layer in the stackup. For multi-layer boards where only one layer should change, use a JobSpec file (described below) or pre-edit the project in the UI.Performance and budget controls
--vram-budget-mb MB # pre-flight bail-out if auto-derive
# estimates more VRAM than this
--perf-mode # fixed -20 dB ringdown, convergence off;
# mesh stays at the chosen quality
--max-supersteps N # hard cap on backend supersteps;
# implies S-params and radiation are
# NOT physically valid
--no-surface-currents # force-disable surface-current DFT
--no-raw-fields # force-disable raw E/H VTK dumpsThe VRAM budget is the most useful guard for batch sweeps on a mixed-quality folder: any project whose auto-mode estimate exceeds the budget gets a manifest with status=vram_overflow_predicted and the backend is never launched. --perf-mode and --max-supersteps are tuned for the performance harness. Both shorten time-stepping without changing the mesh, so the kernel hot path stays exercised while S-parameter accuracy is reduced or unusable.
Save behaviour
--save-after-run # write the project back to disk
--save-as path/to/new.rfsim # save under a different name
# (implies --save-after-run)An updated project carries the embedded run results (S-parameters, radiation pattern, raw port time-series) so re-opening the file in the desktop UI shows the same plots without re-running. Skipping --save-after-run leaves the source project file untouched on disk. The run still writes its own artifacts and manifest to the output directory.
Output directory
--out-dir path/to/results
# Defaults to <project_dir>/_sim_result/<case>/Every run writes a run_manifest.json here covering the timestamp, the backend executable path, the sim params used, the overrides applied, the quality / mode / driving constraint, the measured throughput (duration, supersteps, cells, GCell/s), the convergence history if any, and the list of artifact files produced.
Exports
Each artifact is opt-in. --export-all enables every artifact at once.
--export-sparams all | s11 | s21 | none
--export-touchstone # write .s1p / .s2p alongside the CSV
--export-smith # Smith chart PNG
--export-polar # polar radiation pattern PNG (E + H plane)
--polar-freq-mhz MHZ # target frequency for the polar plot
--export-rad3d # 3D radiation pattern PNG
--rad3d-freq-mhz MHZ # target frequency for the 3D plot
--export-rad3d-vtk # also write the pattern as a VTK file
--export-mesh # mesh preview PNG
--export-mesh-vtk # also write geometry.vtk
--export-currents # surface-currents heatmap PNG
--currents-freq-mhz MHZ # target frequency for the heatmap
--export-all # enable every artifact above
--no-3d-renders # skip the PyVista off-screen exports
--export-convergence-per-pass # write per-pass S-param PNG + CSV
# when a convergence study ranFrequency-keyed exports pick the nearest available frequency in the simulated band. The plot title annotates the exact value used and the delta from the request. If you omit --polar-freq-mhz (or the equivalent for the other frequency-keyed exports) the band centre is used.
Reprocess subcommand
rayrf reprocess recomputes the S-parameter CSV and PNG from the time-series already embedded in the project file. It does not run the backend, so it is much faster than rayrf run and is the right tool for re-exporting an existing run at a different frequency band or FFT length:
rayrf.exe --cli-v1 reprocess --project foo.rfsim --fmin 1000 --fmax 7000
rayrf.exe --cli-v1 reprocess --folder ./batch --recursive --fft-mult 8Reprocess writes its outputs under <project_dir>/_sim_result/_reprocess/ and saves the project back with the refreshed embedded data. It requires a project that has been run at least once. Running it on a fresh project errors out with a clear message about the missing embedded time-series.
JobSpec files
A JobSpec is a YAML or JSON file that describes one run. Anything you can pass as a run flag has an equivalent field in the spec. Use it when you have more than a handful of overrides to send, or when you want a checked-in record of exactly how a run was configured.
rayrf.exe --cli-v1 run --job spec.yaml
rayrf.exe --cli-v1 run --job spec.json--job takes precedence and the loose flags are ignored. This keeps a remote run deterministic: one file in, one manifest out.Top-level fields
Every field is optional except project. Omitted fields fall back to the project file's saved value or the documented default.
project: ./cases/patch_5p8ghz.rfsim # required, .rfsim or .emflux
save_after_run: false # write the project back to disk
save_as: null # save under a different path; implies save_after_run
out_dir: ./_sim_result # where artifacts and the run manifest go
quality: null # "low" | "medium" | "high" | "very-high"
# or "low-med" | "med-high" | "high-vhigh"
# or a decimal in [0, 3]
quality_frac: null # alternative: slider fraction in [0, 1]
sim_mode: null # "auto" | "advanced"; null leaves it as-is
use_center_freq_for_margins: null # true | false | null
convergence: null # true | false | null (leaves project setting)
perf_mode: false # fixed -20 dB ringdown, convergence off
max_supersteps: null # hard cap; S-params/radiation NOT valid when hit
vram_budget_mb: null # pre-flight bail if auto-mode estimate exceeds this
force_disable_surface_currents: false # ignore the project's surface-current setting
force_disable_save_vtk: false # ignore the project's raw E/H VTK setting
overrides: {} # numeric overrides; see schema below
exports: {} # artifact selection; see schema below`overrides` sub-object
Numeric overrides applied on top of the project. Every key is optional. A missing key means the project's saved value is used.
overrides:
air_around_mm: null # FDTD air margin around the geometry, mm
air_above_mm: null # FDTD air above, mm
air_below_mm: null # FDTD air below, mm
dx_mm: null # Yee cell X, mm
dy_mm: null # Yee cell Y, mm
dz_mm: null # Yee cell Z, mm
timesteps: null # integer
freq_min_mhz: null # band minimum, MHz
freq_max_mhz: null # band maximum, MHz
bc_all: null # "PML" | "PEC", applied to all six faces
eps_r: null # applied to every dielectric layer in the stackup
thickness_mm: null # applied to every dielectric layer
loss_tangent: null # applied to every dielectric layer
fft_length_multiplier: null # zero-padding for the S-parameter FFT, integer >= 1`exports` sub-object
Selects which artifacts the run writes alongside the run manifest. All frequency targets are in MHz. When a *_freq_mhzis null on an export that needs one, the band centre is used.
exports:
sparams: "all" # "all" | "s11" | "s21" | "none"
sparams_touchstone: false # write .s1p / .s2p alongside the CSV
smith: false # Smith chart PNG
polar: false # polar radiation pattern PNG (E + H plane)
polar_freq_mhz: null # target frequency for the polar plot
rad3d: false # 3D radiation pattern PNG
rad3d_freq_mhz: null # target frequency for the 3D plot
rad3d_vtk: false # also write the pattern as a VTK file
mesh_preview: false # mesh preview PNG
mesh_preview_vtk: false # also copy geometry.vtk into out_dir
surface_currents: false # surface-current heatmap PNG at nearest freq
surface_currents_freq_mhz: null # target frequency for the heatmap
surface_currents_vtk: false # reserved; currents are 2D-only today
convergence_per_pass: true # per-pass S-param PNG + CSV when convergence ran
no_3d_renders: false # skip every PyVista off-screen exportWorked YAML example
A spec for a single project run at high quality, with every S-parameter artifact plus a Touchstone file, the Smith chart, the polar pattern at 5.8 GHz, and the project saved back to disk after the run:
# spec.yaml
project: ./cases/patch_5p8ghz.rfsim
save_after_run: true
quality: high
sim_mode: auto
convergence: true
out_dir: ./results/patch_5p8ghz
overrides:
freq_min_mhz: 5000
freq_max_mhz: 6500
exports:
sparams: all
sparams_touchstone: true
smith: true
polar: true
polar_freq_mhz: 5800
convergence_per_pass: trueThe same spec in JSON form is accepted by the loader. Pick whichever format your tooling prefers.
Run manifest
Every run writes a single run_manifest.json into the output directory. The file is the authoritative record of what happened: which project, which backend, which overrides applied, the mesh and frequency band that were actually used, performance numbers, and the list of artifacts produced.
{
"timestamp": "2026-05-13T09:42:11.123456",
"project_file": ".../patch_5p8ghz.rfsim",
"backend_exe": ".../rayrf-backend.exe",
"run_dir": ".../results/patch_5p8ghz",
"git_hash": null,
"quality": {
"mode": "auto",
"value": 2.0,
"name": "high"
},
"sim_mode": "auto",
"save_after_run": true,
"save_as": null,
"overrides_applied": { "freq_min_mhz": 5000, "freq_max_mhz": 6500 },
"domain_mm": {
"x_mm": 0.0, "y_mm": 0.0,
"w_mm": 40.0, "h_mm": 40.0
},
"sim_params": {
"dx_mm": 0.25, "dy_mm": 0.25, "dz_mm": 0.20,
"freq_min_hz": 5.0e9, "freq_max_hz": 6.5e9,
"timesteps": 80000,
"bc_all": "PML",
"pml_thickness_mm": 4.0,
"air_around_mm": 12.0,
"air_above_mm": 12.0,
"air_below_mm": 12.0,
"enable_nf2ff": true,
"enable_sparam_delta_stop": true,
"enable_surface_currents": false
},
"derived_params": { ... }, // auto-mode reasoning trace
"performance": {
"duration": "00:00:42",
"duration_s_numeric": 42.31,
"steps": "20 (4 supersteps x 5000)",
"timesteps_total": 20000,
"cells": "3.21 M",
"ncells": 3214000,
"nx": 160, "ny": 160, "nz": 125,
"throughput_gcells_per_s": "8.12 GCell/s",
"gcells_per_s_numeric": 8.12,
"ringdown_reached": true,
"ringdown_ratio": 1.0e-5,
"vram_used_mb": 4321,
"vram_total_mb": 12288,
"vram_overflow": false,
"vram_alloc_failed": false,
"convergence_history": [ ... ], // present only when a convergence study ran
"convergence_converged": true
},
"artifacts": [
{ "kind": "sparams_csv", "path": "patch_5p8ghz.csv" },
{ "kind": "sparams_png", "path": "patch_5p8ghz.png" },
{ "kind": "touchstone", "path": "patch_5p8ghz.s1p" },
{ "kind": "smith", "path": "patch_5p8ghz_smith.png" },
{ "kind": "polar", "path": "patch_5p8ghz_polar_5800MHz.png" }
],
"error": null // string when the run failed
}The shape is stable within CLI v1. Fields are added at the end of their object when needed. Existing fields keep their key and type. If a numeric field has no value (for example a run that bailed before the backend launched), the field is present and set to null. The error field is null on a clean run and carries a short message (prefixed with the failure category, such as vram_overflow: or vram_overflow_predicted:) when the run failed.
Exit codes
0: every project in the batch ran to completion.erroris null in each manifest.1: at least one project errored out. The traceback is on stderr and the matching manifest has a non-nullerrorstring.2: the invocation itself was wrong (missing license, unknown subcommand, missing required flag). No project runs in this case.
Parameter sweeps
The CLI has no built-in sweep range. There is no flag that takes a start, stop, and step value, and no folded loop inside rayrf run. Sweeps are driven by an external script that decides the parameter values, prepares the input for each point, calls rayrf run per point, and reads the resulting run_manifest.json files. Pick the variant that matches your input style:
1. Sweep by override flag
When the parameter you want to sweep is one of the numeric --* overrides (frequency band, dielectric eps_r, thickness, Yee cell, time-steps), the script is a loop that calls rayrf run once per point. The source project file is read-only. Only the override changes between points. Example in PowerShell:
# Sweep substrate thickness from 0.5 mm to 2.0 mm in 0.25 mm steps.
$Project = ".\cases\patch_5p8ghz.rfsim"
$OutRoot = ".\results\thickness_sweep"
foreach ($t in 0.50, 0.75, 1.00, 1.25, 1.50, 1.75, 2.00) {
$tag = "t_{0:N2}mm" -f $t
$outdir = Join-Path $OutRoot $tag
& "rayrf.exe" --cli-v1 run --project $Project --thickness $t `
--quality high --export-sparams all --export-touchstone `
--out-dir $outdir
if ($LASTEXITCODE -ne 0) {
Write-Warning "Run failed at thickness $t mm (exit $LASTEXITCODE)"
}
}The same shape in bash, for Git Bash or WSL users:
#!/usr/bin/env bash
project="./cases/patch_5p8ghz.rfsim"
out_root="./results/thickness_sweep"
for t in 0.50 0.75 1.00 1.25 1.50 1.75 2.00; do
tag="t_${t}mm"
rayrf.exe --cli-v1 run \
--project "$project" \
--thickness "$t" \
--quality high \
--export-sparams all \
--export-touchstone \
--out-dir "$out_root/${tag}" \
|| echo "Run failed at thickness $t mm"
done2. Sweep by JobSpec-per-point
When the sweep touches fields that have no convenient top-level CLI flag, or when you want a checked-in record of each point's configuration, write one JobSpec per point and feed them in. Python is convenient:
# sweep_eps.py - generate one JobSpec per epsilon_r value, then run each.
import subprocess, yaml
from pathlib import Path
points = [2.2, 3.0, 3.66, 4.4, 4.7, 6.15] # the parameter range
project = Path("./cases/patch_5p8ghz.rfsim").resolve()
specs_dir = Path("./specs/eps_sweep"); specs_dir.mkdir(parents=True, exist_ok=True)
results_dir = Path("./results/eps_sweep"); results_dir.mkdir(parents=True, exist_ok=True)
for eps in points:
tag = f"eps_{eps:.2f}"
spec = {
"project": str(project),
"quality": "high",
"sim_mode": "auto",
"out_dir": str(results_dir / tag),
"overrides": { "eps_r": eps },
"exports": { "sparams": "all", "sparams_touchstone": True }
}
spec_path = specs_dir / f"{tag}.yaml"
spec_path.write_text(yaml.safe_dump(spec, sort_keys=False))
rc = subprocess.call([
"rayrf", "--cli-v1", "run", "--job", str(spec_path)
])
if rc != 0:
print(f"Run failed at eps={eps} (exit {rc})")3. Sweep by folder of saved variants
When the sweep touches fields that are project-state only (port location, primitive geometry, layer count), the only honest path is to save one project file per point in the UI ahead of time, then run the whole folder. The --folder form walks every supported project file in alphabetical order and runs them sequentially.
rayrf.exe --cli-v1 run \
--folder ./sweeps \
--recursive \
--quality high \
--export-sparams all \
--export-touchstone \
--out-dir ./results/folder_sweepThe folder mode writes one run_manifest.json per project (under the per-case output sub-folder), so a follow-up script can aggregate by reading the manifests.
Aggregating results
Each point's output directory carries a run_manifest.json. Pull the fields you care about and cross-reference with the parameter value to plot the sweep. Example in Python:
import json
from pathlib import Path
rows = []
for manifest in Path("./results/thickness_sweep").rglob("run_manifest.json"):
m = json.loads(manifest.read_text())
perf = m.get("performance", {})
rows.append({
"out_dir": m.get("run_dir"),
"thickness_mm": m.get("overrides_applied", {}).get("thickness_mm"),
"duration_s": perf.get("duration_s_numeric"),
"ncells": perf.get("ncells"),
"throughput": perf.get("gcells_per_s_numeric"),
"error": m.get("error"),
})
# rows is now a flat list you can hand to pandas, csv.DictWriter, etc.rayrf run processes from your script in parallel, each with its own --out-dir. Within a single GPU, the backend already uses the full device. Running two backends on one card will serialize on VRAM and be slower than one.Troubleshooting
- License required for headless mode: the CLI capability is gated by plan tier. Run
auth --statusto see what the local cache thinks. If it says Hobby or Viewer and you have a Pro subscription, runauth --license-keywith the key from the account dashboard to refresh the binding. - Backend executable not found: the autodetector only walks two locations. Pass
--backendexplicitly when running a non-installed build. - vram_overflow_predicted: the auto-derive engine estimated the run will not fit in the configured budget. Lower the quality, narrow the frequency band, or raise
--vram-budget-mbif you know the estimate is conservative. - vram_overflow at run time: the backend hit a hard ceiling mid-run. The CLI sets a sentinel file so the backend stops gracefully. If it does not stop within 8 seconds the process is terminated. The manifest carries enough memory telemetry to diagnose.
- Activation fails with trial_conflict: the machine fingerprint has already consumed its 30 day trial under a different account. Subscribe to continue, or contact
support@rayrf.comwith the machine id fromauth --status.
@@TELEM: JSON lines on stdout for per-step telemetry. These are parsed and suppressed by default. Set RAYRF_CLI_SHOW_TELEM=1 in the environment to also print them to the console for debugging.