Open-source charged particle optics toolkit
Project description
IonForge
Open-source charged particle optics toolkit.
The SDK provides parametric geometry primitives for building simulation meshes, Pydantic v2 serialization models, STL mesh import/export with quality metrics, and a client for the IonForge cloud API.
Installation
uv add ionforge
Quick Start
from ionforge.geometry import Geometry, Cylinder, AnnularDisk
geo = Geometry(bounding_box=(0.1, 0.1, 0.2))
geo.add(Cylinder(r=0.01, length=0.05, voltage=100, name="tube_1"))
geo.add(AnnularDisk(inner_radius=0.005, outer_radius=0.01, voltage=0, name="aperture"), z=0.06)
geo.add(Cylinder(r=0.01, length=0.05, voltage=50, name="tube_2"), z=0.07)
serialized = geo.to_serialized_geometry()
errors = serialized.validate_consistency() # [] if valid
Examples
Runnable scripts are in the examples/ directory. Run any of them with:
uv run python examples/<name>.py
Parametric primitives
Build geometry using Cylinder, AnnularDisk, Cone, and Sphere primitives. Each primitive takes a voltage, name, and n_segments (default 32) for mesh resolution.
Primitives extrude along the z axis (the optical axis) and are centred on it in x/y; lengths and positions are in metres. The axes, origin, and bounding-box conventions are documented in docs/parameters.md.
from ionforge.geometry import Geometry, Cylinder, AnnularDisk, Cone, Sphere
geo = Geometry(bounding_box=(0.1, 0.1, 0.3))
# Drift tube
geo.add(Cylinder(r=0.01, length=0.05, voltage=100, name="tube_1"))
# Aperture plate
geo.add(
AnnularDisk(inner_radius=0.003, outer_radius=0.01, voltage=0, name="aperture"),
z=0.06,
)
# Tapered section
geo.add(
Cone(bottom_radius=0.01, top_radius=0.005, length=0.03, voltage=-50, name="taper"),
z=0.07,
)
# Second tube
geo.add(Cylinder(r=0.005, length=0.05, voltage=100, name="tube_2"), z=0.10)
serialized = geo.to_serialized_geometry()
See examples/build_geometry.py for a full runnable version.
STL import and export
Load a mesh from an STL file, inspect quality metrics, and re-export.
from ionforge.geometry.stl_import import load_stl, mesh_stats, write_stl
# Load STL with mm -> metres conversion
triangles = load_stl("model.stl", scale_factor=1e-3)
# Print mesh quality statistics
stats = mesh_stats(triangles, verbose=True)
# Mesh statistics:
# Triangles: 2 (0 degenerate, 2 valid)
# Total area: 1.00 mm²
# Edge range: 1.000 – 1.414 mm
# Aspect ratio: mean=2.00 max=2.00
# Export as binary STL
write_stl("output.stl", triangles, name="my_mesh")
See examples/stl_round_trip.py for a self-contained runnable version.
Visualization
Geometry can be rendered in 3-D with three backends: matplotlib (default, no extra deps), plotly (interactive, great for Jupyter/Colab), and pyvista (full VTK).
geo = Geometry(bounding_box=(0.06, 0.06, 0.12))
geo.add(Cylinder(r=0.01, length=0.03, voltage=0, name="tube"))
# ... build geometry ...
geo.visualize() # matplotlib (default)
geo.visualize(backend="plotly") # interactive HTML widget
geo.visualize(backend="pyvista") # VTK 3-D window
Color by group name or by voltage (auto-selected when every group has a voltage):
geo.visualize(color_by="voltage") # blue–white–red diverging colourmap
geo.visualize(color_by="group") # per-group hex colours
| Colour by voltage | Colour by group |
|---|---|
You can also render a SerializedGeometry directly:
from ionforge.geometry.visualization import render
render(serialized, backend="plotly", color_by="voltage", opacity=0.8)
Install optional visualization backends:
uv add ionforge --extra viz-plotly # plotly only
uv add ionforge --extra viz-pyvista # pyvista only
uv add ionforge --extra viz # all visualization deps
Per-backend examples:
examples/viz_matplotlib.py— static 3-D plot (no extra deps)examples/viz_plotly.py— interactive HTML widgetexamples/viz_pyvista.py— full VTK 3-D window
See also examples/visualize_geometry.py for a CLI version with --backend and --color-by flags.
JSON round-trip
Geometry models use snake_case in Python and camelCase when serialized to JSON. Both naming conventions are accepted when parsing.
import json
from ionforge.geometry import SerializedGeometry
# Serialize to camelCase JSON
camel_json = json.dumps(serialized.model_dump(by_alias=True), indent=2)
# {"version": 1, "units": "m", "boundingBox": {...}, "groupOrder": [...], ...}
# Parse from camelCase
parsed = SerializedGeometry.model_validate_json(camel_json)
# Parse from snake_case (also works)
parsed = SerializedGeometry.model_validate({
"version": 1,
"units": "m",
"vertices": [],
"edges": [],
"faces": [],
"bounding_box": {"size": [1, 1, 1], "voltage": 0},
"groups": [],
"group_order": [],
})
See examples/json_round_trip.py for the full runnable version.
JSON Schema generation
Every serialization model is a Pydantic model, so you can emit a standard JSON Schema straight from it - handy for validation or for generating types in other languages:
import json
from ionforge.geometry import SerializedGeometry
schema = SerializedGeometry.model_json_schema()
print(json.dumps(schema, indent=2))
See examples/export_schema.py for a runnable version that writes the schema to stdout:
uv run python examples/export_schema.py > geometry-schema.json
Low-level model API
For full control, construct SerializedGeometry directly from vertices, edges, faces, and groups:
from ionforge.geometry import (
BoundingBox, Edge, Face, Group, SerializedGeometry, Vertex,
)
geo = SerializedGeometry(
vertices=[
Vertex(id="v0", position=(0.0, 0.0, 0.0)),
Vertex(id="v1", position=(0.1, 0.0, 0.0)),
Vertex(id="v2", position=(0.05, 0.1, 0.0)),
],
edges=[
Edge(id="e0", v0="v0", v1="v1", face_ids=["f0"]),
Edge(id="e1", v0="v1", v1="v2", face_ids=["f0"]),
Edge(id="e2", v0="v2", v1="v0", face_ids=["f0"]),
],
faces=[
Face(id="f0", vertex_ids=["v0", "v1", "v2"], edge_ids=["e0", "e1", "e2"]),
],
bounding_box=BoundingBox(size=(0.2, 0.2, 0.2), voltage=0.0),
groups=[
Group(id="g0", name="plate", color="#ff0000", voltage=10.0, face_ids=["f0"]),
],
group_order=["g0"],
)
errors = geo.validate_consistency() # [] if valid
API client
The SDK ships an optional client for the IonForge cloud API. Install it with the client extra:
uv add "ionforge[client]"
Authenticate with an API key. The client reads IONFORGE_API_KEY from the environment (or pass api_key= explicitly), and an optional IONFORGE_BASE_URL overrides the API endpoint:
export IONFORGE_API_KEY="ifk_..."
The workflow is geometry -> model -> run. Build a geometry locally, upload it, launch a simulation run, and pull down the results:
from ionforge.client import IonForge
from ionforge.geometry import Geometry, Cylinder
with IonForge() as client: # reads IONFORGE_API_KEY
project = client.projects.create(name="Einzel lens study")
geo = Geometry(bounding_box=(0.1, 0.1, 0.2))
geo.add(Cylinder(r=0.01, length=0.05, voltage=100, name="tube"))
geometry = client.upload_geometry(project.id, "lens-v1", geo)
# Creates a model, launches a run, and waits for it to finish
run = client.run_simulation(
project_id=project.id,
name="baseline",
geometry_id=geometry.id,
)
paths = client.download_results(run.id, output_dir="results/")
An AsyncIonForge client with the same surface is available for asyncio code.
Analysing results with pandas
Install the pandas extra (uv add "ionforge[pandas]") to pull sweep and run results straight into a DataFrame for analysis and plotting. Sweep results give one row per point, with each swept parameter flattened to its own param.-prefixed dot-path column (param.beam.E_nominal) alongside the objective and per-point status:
with IonForge() as client:
# One row per sweep point; auto-paginates across result cursors.
results = client.sweeps.list_results(sweep.id, mode="full")
df = results.to_dataframe()
# e.g. transmission vs beam energy
df.plot.scatter(x="param.beam.E_nominal", y="summary.transmission")
# A tabular view of every run in a project
runs_df = client.runs.to_dataframe(project_id=project.id)
In full mode, per-point result-summary metrics appear as summary.* columns; table mode returns just the objective and status. Pass max_rows= to cap large pulls.
Downloaded result files parse into numpy arrays and DataFrames with load_result. Each result file carries the scalar run metrics (transmission, energy resolution, point spread), the per-particle exit and input arrays, and -- when the run stored them -- per-particle trajectories:
from ionforge.client import IonForge, load_result
with IonForge() as client:
paths = client.download_results(run.id, output_dir="results/")
data = load_result(paths[0])
if data.summary.transmission is not None:
print(f"transmission: {data.summary.transmission:.1%}")
eres = data.summary.energy_resolution
if eres is not None and eres.fwhm_eV is not None:
print(f"energy resolution FWHM: {eres.fwhm_eV:.3f} eV")
# Per-particle numbers as numpy arrays.
exit_energy_spread = data.exit_energies.std()
# Stable per-particle frames: one row per launched particle, and one row
# per transmitted particle. Their shapes don't flip when particles are lost.
particles = data.particles_dataframe() # column: input_energy
exits = data.exits_dataframe() # columns: exit_energy, exit_position
# The energy-transmission curve, when present.
curve = data.transmission_curve_dataframe()
client.load_results(run.id, output_dir="results/") downloads and parses in one call, returning a RunResultData per file.
Simulation runs are configured with ModelParams (beam, solver, integrator, and more). Every field, with its units, defaults, and conventions, is documented in docs/parameters.md.
See examples/run_simulation.py for a complete, runnable end-to-end workflow that builds an einzel lens, uploads it, runs a simulation, and downloads the results.
Development
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run ty check
If you use an AI coding assistant (Claude Code, Cursor, etc.), see AGENTS.md for a concise, machine-readable orientation to the SDK's layout and conventions.
See RELEASING.md for how versions are cut and published to PyPI.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ionforge-0.3.0.tar.gz.
File metadata
- Download URL: ionforge-0.3.0.tar.gz
- Upload date:
- Size: 505.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
576ecc9da488f3b9716510db89c086a24c3d015e4050b3d8ed336482987d7f02
|
|
| MD5 |
0b41605263c7714c8f3179f3b63735f0
|
|
| BLAKE2b-256 |
e6509d5a3180665eb36b4c84e32590bec67c0c4be5c53c8ab5eedb26b920b29d
|
Provenance
The following attestation bundles were made for ionforge-0.3.0.tar.gz:
Publisher:
release.yml on ionforge-labs/ionforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ionforge-0.3.0.tar.gz -
Subject digest:
576ecc9da488f3b9716510db89c086a24c3d015e4050b3d8ed336482987d7f02 - Sigstore transparency entry: 2186543125
- Sigstore integration time:
-
Permalink:
ionforge-labs/ionforge@83e4d17dede501cdd2b18612401c8f5f595350f8 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ionforge-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@83e4d17dede501cdd2b18612401c8f5f595350f8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file ionforge-0.3.0-py3-none-any.whl.
File metadata
- Download URL: ionforge-0.3.0-py3-none-any.whl
- Upload date:
- Size: 64.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ba1012cafb56dab8c9efa420a237af4daed585e7f4b67430313503692661ffc
|
|
| MD5 |
22b892c22c20cefa089a01f69a69b2f1
|
|
| BLAKE2b-256 |
10cdbc503f82f338330bfec79e65063800524c13668366656e2240bd971172de
|
Provenance
The following attestation bundles were made for ionforge-0.3.0-py3-none-any.whl:
Publisher:
release.yml on ionforge-labs/ionforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ionforge-0.3.0-py3-none-any.whl -
Subject digest:
4ba1012cafb56dab8c9efa420a237af4daed585e7f4b67430313503692661ffc - Sigstore transparency entry: 2186543181
- Sigstore integration time:
-
Permalink:
ionforge-labs/ionforge@83e4d17dede501cdd2b18612401c8f5f595350f8 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/ionforge-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@83e4d17dede501cdd2b18612401c8f5f595350f8 -
Trigger Event:
release
-
Statement type: