Skip to main content

mikro

codecov PyPI version PyPI pyversions PyPI download month

The Python client for mikro, the Arkitekt service for microscopy and imaging data. mikro-server is a GraphQL server that keeps the metadata of your data (datasets, folders, coordinate systems, scenes, and the relations between them), while the pixels live in object storage as Zarr (and tables as Parquet). This client gives you both as one: typed, validated Python objects built on rath and pydantic, whose .data is a lazy xarray backed by dask. When you pass an array to a mutation, mikro uploads it before the call runs, and when you read one back it downloads only the chunks you touch.

Installation

pip install mikro

mikro requires Python 3.11+. Extras bring in the heavier wire formats only when you need them:

Extra Brings in
table pyarrow, pandas and duckdb, for querying Parquet-backed tables
mesh fabriks, for uploading meshes
network konnektion, for uploading graphs
sparse sporadik, for sparse datasets
complete all of the above

Inside an arkitekt app, pip install "arkitekt[rekuest,mikro]" installs mikro along with arkitekt and the runtime run(app) needs.

Usage

Every mikro operation is a method of the Mikro client. Each comes in a blocking and an a-prefixed async flavour (mikro.create_folder(...), await mikro.acreate_folder(...)). An object that a call returns remembers the client that fetched it, so later calls on it, such as .data, go through the same client.

In an arkitekt app

Add mikro_service to your app and take the client by annotation. arkitekt injects it. Types from mikro.arkitekt.specs such as Volume or Image are lenses over a mikro dataset. Each one declares what an action needs, and the UI then offers only datasets that fit. A run tests the same on every lens an action takes or returns: one that does not fit the type fails the task, saying why.

from arkitekt import App, Task, run
from mikro import Mikro, mikro_service
from mikro.arkitekt.specs import Volume

app = App("clip-volume", "0.1.0", services=[mikro_service])


@app.action
def clip(volume: Volume, mikro: Mikro, task: Task) -> Volume:
    """Clip Volume

    Clips negative values and stores the result on the same grid.
    """
    source = volume.data  # a lazy xarray.DataArray
    task.progress(30, f"Processing {source.shape}")
    clipped = source.clip(min=0).compute()

    result = mikro.create_array_dataset(
        data=clipped,
        scales=[],
        name="clipped",
        axes=volume.carried_axes(clipped.dims),
        derived_from=[volume.derive_identity(value_relation="TRANSFORMED")],
    )
    return result.lens()


if __name__ == "__main__":
    run(app)

Datasets, tables, meshes, annotations, lenses, coordinate systems, scenes, folders and files travel between actions by id (@mikro/arraydataset, @mikro/arraylens for an array lens, @mikro/tablelens, @mikro/scene, ...), so an action can take them and return them directly.

From a script

easy connects for you and returns the client:

import numpy as np
import xarray as xr

from arkitekt import easy
from mikro import dataset_arrays, mikro_service

volume = xr.DataArray(np.random.random((2, 10, 256, 256)), dims=("c", "z", "y", "x"))

with easy("my-script", mikro_service) as mikro:
    folder = mikro.create_folder(name="examples")

    # `data` is level 0; `scales` holds only the coarser levels
    level_zero, scales = dataset_arrays(volume, levels=3)

    dataset = mikro.create_array_dataset(
        data=level_zero,
        scales=scales,
        name="random volume",
        axes=["c", "z", "y", "x"],
        folder=folder.id,
    )

    again = mikro.get_array_dataset(dataset.id)
    peak = again.data.max().compute()  # downloads only what it needs

Use async with aeasy(...) in async code, or interactive("notebook", mikro_service) in Jupyter. Inside an async loop, remember that computing .data downloads the data in a blocking way on that loop.

Declared axes are checked against the array before it is uploaded. For example, a rank mismatch or a missing declaration raises ArrayDeclarationError (tables and sparse datasets have their own errors) before any data is sent.

A camera frame, and tiles that sit side by side

A frame straight off a camera is a bare array. Three things make it an image someone can look at: labelled axes, contrast limits to draw it with, and a place in physical space.

import numpy as np
import xarray as xr

from mikro import Unit, canonical, dataset_arrays, space_2d
from mikro.api.schema import CoordinateAnchorInput, ScenePolicyInput


def upload(mikro, frame: np.ndarray, name: str, space, x_um: float, y_um: float, pixel_um: float):
    # 1. Name the axes. `canonical` puts them in the order the server wants (c, y, x).
    image = canonical(xr.DataArray(frame, dims=("y", "x", "c")[: frame.ndim]))

    # 2. A pyramid, so a large frame opens at once, and one contrast anchor per channel.
    levels = max(1, min(4, int(np.log2(max(frame.shape[:2]) / 512)) + 1))
    level_zero, scales = dataset_arrays(image, levels=levels, method="mean")
    dataset = mikro.create_array_dataset(
        data=level_zero,
        scales=scales,
        name=name,
        axes=[str(d) for d in image.dims],
        anchors=CoordinateAnchorInput.histogram_anchors(image),
    )

    # 3. Where it is: pixel size as the scale, the stage position as the offset.
    space.register(dataset, scale={"y": pixel_um, "x": pixel_um}, x=x_um, y=y_um)
    return dataset


space = space_2d(mikro, "tile scan", unit=Unit("micrometer"))
for x_um, y_um, frame in tiles:
    upload(mikro, frame, f"tile {x_um:.0f} {y_um:.0f}", space, x_um, y_um, pixel_um=0.65)

# One scene over the space: every tile at its position.
space.stage(name="tile scan", policy=ScenePolicyInput(nchildren=len(tiles)))

Units and positions never go on the dataset: a physical space is a coordinate system of its own, and register says where the data sits in it. Tiles registered into one space share one world, which is what makes a viewer show them stitched by position. A single frame gets a space of its own the same way.

stage() builds a scene from at most nchildren registered sources, 8 unless you say otherwise. A scan of more tiles than that needs the policy above, or the scene silently shows the first eight.

An action that returns the image returns a lens, not the dataset: return dataset.lens().

Working with data

data = dataset.data               # level 0, a lazy dask-backed xarray.DataArray
coarse = dataset.level_data(2)    # a coarser pyramid level
levels = dataset.multi_scale_data()
window = dataset.lens(z=(0, 5))   # an ArrayLens: an immutable selection over the dataset
window.data
whole = dataset.lens()            # the dataset's whole lens: read, never made

A lens is one row per container and selection, so the same selection asked for twice is the same lens, and its slices read back the way the server normalized them. Lens itself is the interface over six kinds. An ArrayLens selects pixels by index slices; tables, sparse datasets, meshes, networks and annotation collections are selected by value windows instead (table.lens(t=(0, 10)), expression.lens(gene=4711), collection.lens() for the whole of one), and a layer names a lens and nothing narrower: mikro.create_point_layer(scene=scene.id, lens=table.lens().id). That holds for a channel too. An intensity, label or phasor layer draws a lens pinning one channel (dataset.lens(c=0)), an RGB layer names three of them, and a SPARSE colouring names the sparse lens that is its slice. Which axis of a lens faces the screen is the layer's to say: layer.render_axes.

A chart: data along one axis

A scene is a place; a chart is one metric axis with values read off it. It adopts a space with exactly one unit-carrying axis, and a layer is drawn where the edge into that space lays it:

from mikro.api.schema import AxisType, PhysicalAxisInput

chart = mikro.create_chart(name="ΔF/F", axis=PhysicalAxisInput(name="t", type=AxisType.TIME, unit=Unit("second")))
chart.world.register(traces, scale={"t": 0.05})        # a (cell, t) array, 50 ms per sample

layer = mikro.create_trace_chart_layer(chart=chart.id, lens=traces.lens().id)
layer.along_axis, layer.series_axis                    # ("t", "cell"): one line per cell

create_series_chart_layer draws a table's value column, create_annotation_chart_layer makes a surface to draw marks on, and space.chart() bootstraps a chart from what is already registered.

examples/ holds ten self-contained scripts. Each one simulates an imaging modality (SMLM, calcium imaging, high-content screening, CLEM, MRI, tractography, ...) and composes it into a mikro scene, and together they use every layer kind the API offers.

Metrics: one value about an annotation or a dataset

A metric is an observation: what a process computed or someone counted, about an annotation, an array, a table or a sparse dataset. It is a quantity (magnitude in a unit pint reads) or a code a process emitted, never both, under a key namespaced by whoever measured it. Hand the object itself as the target, a nested one included (target=lens.dataset): every generated class of a type the server identifies carries its structure as __identifier__.

from mikro.api.schema import MetricFilter, MetricInput, MetricTargetFilter

mikro.create_metric(target=dataset, key="my_app/mean_intensity", magnitude=812.4, unit="dimensionless")
mikro.create_metric(target=annotation, key="segmenter_v2/class", code="segmenter_v2/class_3")

# Up to 5000 in one transaction. The batch sends no signal; mikro's change feed still has every one.
mikro.create_metrics(metrics=[MetricInput(target=dataset, key="my_app/area", magnitude=a, unit="µm**2") for a in areas])

Inside a MetricInput the object is coerced all the same, but a type checker sees the field as MetricTargetInput and flags it; the spelling it accepts is MetricTargetInput(identifier="@mikro/arraydataset", object=dataset.id).

A metric is immutable: a correction is a new one that supersedes it (mikro.create_metric(..., supersedes=wrong)), and the corrected one stays as long as the correction does. An object that has a metric cannot be deleted until the last one is. Which app recorded a metric, and in which task, is read off the request; you never send it. mikro.list_metrics(filters=MetricFilter(target=MetricTargetFilter(identifier="@mikro/arraydataset", object=dataset.id))) reads back what was recorded about one object. A metric also travels between actions as @mikro/metric.

Standalone

Outside arkitekt, you build the client yourself from a MikroRath and a DataLayer, and you use it as a context manager:

from mikro import Mikro
from mikro.datalayer import DataLayer
from mikro.rath import MikroRath

mikro = Mikro(rath=MikroRath(link=...), datalayer=DataLayer(...))

with mikro:
    folder = mikro.create_folder(name="examples")

tests/stack.py shows the complete wiring against a local deployment.

Prerequisites

mikro needs a running mikro-server to connect to. The easiest option is an Arkitekt deployment. For a local test server, tests/integration/docker-compose.yml starts mikro together with a database and S3-compatible object storage.

Development

The generated API (mikro/api/schema.py) is produced by turms from the schema and the documents in graphql/ (see graphql.config.yaml).

uv run pytest -m "not integration"   # no server needed
uv run pytest -m integration          # a real mikro deployment via dokker

See RELEASING.md for how versions are cut.

License

MIT, see LICENSE.

Metadata

Release files for mikro 8.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mikro 8.2.0
File Size Uploaded Provenance
mikro-8.2.0.tar.gz 443.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mikro 8.2.0
File Interpreter ABI Platform
mikro-8.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 903.9 kB

Release files / mikro-8.2.0.tar.gz

Download URL mikro-8.2.0.tar.gz
Size 443.0 kB
Tags Source
SHA-256 checksum
How to use checksums
74645aa79142ae6b1b210e3199b0430568fcb2798339a4576861da75ae8c6fc2
BLAKE2b-256 checksum
How to use checksums
e5ad215d4afe8720c2afba2c1e8f41a3f28184ef25659d4ab516373d34069961
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / mikro-8.2.0-py3-none-any.whl

Download URL mikro-8.2.0-py3-none-any.whl
Size 460.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ab499afe252e35ceb987fd15232290ae32214129409615a850f18653effb9885
BLAKE2b-256 checksum
How to use checksums
f8ed1d0820dd93e8bc5d74d4d8dcf55ae0d2a81fbf4e2f7059b543466750fd15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for mikro 8.2.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

8.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page