Skip to main content

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
voltage 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:

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ionforge-0.3.0.tar.gz (505.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ionforge-0.3.0-py3-none-any.whl (64.4 kB view details)

Uploaded Python 3

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

Hashes for ionforge-0.3.0.tar.gz
Algorithm Hash digest
SHA256 576ecc9da488f3b9716510db89c086a24c3d015e4050b3d8ed336482987d7f02
MD5 0b41605263c7714c8f3179f3b63735f0
BLAKE2b-256 e6509d5a3180665eb36b4c84e32590bec67c0c4be5c53c8ab5eedb26b920b29d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ionforge-0.3.0.tar.gz:

Publisher: release.yml on ionforge-labs/ionforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for ionforge-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4ba1012cafb56dab8c9efa420a237af4daed585e7f4b67430313503692661ffc
MD5 22b892c22c20cefa089a01f69a69b2f1
BLAKE2b-256 10cdbc503f82f338330bfec79e65063800524c13668366656e2240bd971172de

See more details on using hashes here.

Provenance

The following attestation bundles were made for ionforge-0.3.0-py3-none-any.whl:

Publisher: release.yml on ionforge-labs/ionforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page