Skip to content

Field Registry and Compute

FieldDataset.compute("beta") dispatches through this registry: a Recipe names the derived function, the canonical field names it consumes, and the quantity type of what it returns. RECIPES is the registry itself, exposed read-only.

register_recipe / unregister_recipe extend it at runtime, and SPECIES_TEMPLATES synthesizes per-species entries (omega_p_s2, lambda_D_s3, ...) on demand rather than enumerating every species up front.

Adding a quantity goes through register_recipe, not Recipe. It takes the name and quantity_type alongside the function and its inputs, and registers both the recipe and the field metadata, so compute(), in_si() and field_info() all light up together:

register_recipe(
    "e_mag_fraction",
    func=lambda e_b, e_k: e_b / (e_b + e_k),
    fields=("e_B", "e_k"),          # stored fields or other derived names
    quantity_type="dimensionless",
)

Recipe is the record the registry stores. It carries neither the name nor the quantity type — those are registry keys — and is exported for the cross-language export in pypic.codegen, not for registration. For a single array on a single dataset, FieldDataset.with_field is the lighter option: it stamps the metadata into that dataset's attrs and leaves the global registry alone. examples/advanced_calculations.py runs both.

field_dependencies reports what a quantity needs, which is what lets Simulation.read load exactly the fields a later compute() call will require. available_quantities() takes no arguments and lists every registered quantity name and alias, excluding the per-species names synthesized on demand.

Recipes marked supports_relativistic=True receive c automatically when physics.relativistic is set in the dataset config — see Equations § 8.

compute

String-based dispatch for derived quantities on FieldDataset.

Maps short names ("|B|", "beta", "v_A", ...) to pure functions in derived.py, diagnostics.py, and operators.py. The tables live in pypic._recipes; this module resolves names, executes recipes, and owns the registration API. FieldDataset sits below it and reaches compute_field through a deferred import.

Recipe dataclass

Describes how to derive one quantity from existing fields.

Mapped from a canonical name in RECIPES. func consumes the dependency arrays declared in fields (in order) and returns the derived array. The remaining attributes describe what extras the dispatcher should inject (grid, gamma, species args, …) before calling func.

Source code in src/pypic/_recipes.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@dataclass(frozen=True, slots=True)
class Recipe:
    """Describes how to derive one quantity from existing fields.

    Mapped from a canonical name in `RECIPES`. ``func`` consumes
    the dependency arrays declared in ``fields`` (in order) and returns
    the derived array. The remaining attributes describe what extras
    the dispatcher should inject (grid, gamma, species args, …) before
    calling ``func``.
    """

    func: Callable[..., Any]
    fields: tuple[str, ...]
    species_index: int | None = None
    needs_grid: bool = False
    needs_gamma: bool = False
    needs_c: bool = False
    component: int | None = None
    species_args: SpeciesArgs | None = None
    # When True the dataset's geometry reaches ``func`` as a ``geometry=``
    # kwarg — the operator-backed recipes (``div_B``, ``div_E``,
    # ``curl_B*``, ``vort*``).  ``psi`` opts out: ``magnetic_flux_function``
    # documents its own Cartesian-only generalization.
    passes_geometry: bool = False
    # When True and ``physics.relativistic`` is set on the dataset,
    # ``c`` is injected as a keyword argument, activating the
    # relativistic branch of functions with a ``c=None`` kwarg.
    supports_relativistic: bool = False

SpeciesArgs

Bases: StrEnum

Describes which species parameters a dynamic recipe needs.

Source code in src/pypic/_recipes.py
24
25
26
27
28
29
30
class SpeciesArgs(StrEnum):
    """Describes which species parameters a dynamic recipe needs."""

    CHARGE_MASS = "charge_mass"
    MASS_ONLY = "mass_only"
    CHARGE_ONLY = "charge_only"
    NONE = "none"

SpeciesTemplate dataclass

Template for species-dependent derived quantities.

Used to dynamically synthesize recipes for species index >= 2, where static registry entries don't exist.

Source code in src/pypic/_recipes.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
@dataclass(frozen=True, slots=True)
class SpeciesTemplate:
    """Template for species-dependent derived quantities.

    Used to dynamically synthesize recipes for species index >= 2,
    where static registry entries don't exist.
    """

    func: Callable[..., Any]
    field_pattern: tuple[str, ...]
    species_args: SpeciesArgs
    needs_gamma: bool = False
    needs_c: bool = False
    # Mirrors ``Recipe.component`` for tuple-returning funcs like
    # ``perpendicular_vector``.  The synthesized ``Recipe`` carries it
    # through so the compute path selects the right tuple element.
    component: int | None = None
    supports_relativistic: bool = False

compute_field(name, dataset, _depth=0)

Compute a derived quantity by name from a FieldDataset.

If name is already present in the dataset, returns it directly. Otherwise dispatches to the registered pure function, recursively resolving any intermediate dependencies.

Parameters:

Name Type Description Default
name str

Field or derived quantity name (e.g. "|B|", "beta").

required
dataset FieldDataset

Source data.

required

Returns:

Type Description
FloatArray

Computed array in code units.

Raises:

Type Description
KeyError

If name is unknown and not in the dataset.

ValueError

If required species or physics info is missing.

GeometryUnsupportedError

If the recipe requires spatial derivatives and the dataset grid is non-Cartesian or not three-dimensional. Subclass of NotImplementedError.

RecursionError

If dependency chain exceeds depth limit.

Examples:

Dependencies resolve recursively — v_A needs |B|, which the dataset does not carry either:

>>> import numpy as np
>>> from pypic import FieldDataset, GridInfo
>>> from pypic.units import Normalization
>>> grid = GridInfo(dimensions=(2, 2, 2), spacing=(1.0, 1.0, 1.0))
>>> ones = np.ones((2, 2, 2))
>>> ds = FieldDataset.from_arrays(
...     {"B_1": 3.0 * ones, "B_2": 4.0 * ones, "B_3": 0.0 * ones,
...      "rho_m": 4.0 * ones},
...     grid, Normalization.identity(),
... )
>>> float(compute_field("v_A", ds)[0, 0, 0])  # |B| / sqrt(rho_m)
2.5
Source code in src/pypic/compute.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def compute_field(name: str, dataset: FieldDataset, _depth: int = 0) -> FloatArray:
    """Compute a derived quantity by name from a FieldDataset.

    If *name* is already present in the dataset, returns it directly.
    Otherwise dispatches to the registered pure function, recursively
    resolving any intermediate dependencies.

    Parameters
    ----------
    name : str
        Field or derived quantity name (e.g. ``"|B|"``, ``"beta"``).
    dataset : FieldDataset
        Source data.

    Returns
    -------
    FloatArray
        Computed array in code units.

    Raises
    ------
    KeyError
        If *name* is unknown and not in the dataset.
    ValueError
        If required species or physics info is missing.
    GeometryUnsupportedError
        If the recipe requires spatial derivatives and the dataset
        grid is non-Cartesian or not three-dimensional.  Subclass of
        `NotImplementedError`.
    RecursionError
        If dependency chain exceeds depth limit.

    Examples
    --------
    Dependencies resolve recursively — ``v_A`` needs ``|B|``, which the
    dataset does not carry either:

    >>> import numpy as np
    >>> from pypic import FieldDataset, GridInfo
    >>> from pypic.units import Normalization
    >>> grid = GridInfo(dimensions=(2, 2, 2), spacing=(1.0, 1.0, 1.0))
    >>> ones = np.ones((2, 2, 2))
    >>> ds = FieldDataset.from_arrays(
    ...     {"B_1": 3.0 * ones, "B_2": 4.0 * ones, "B_3": 0.0 * ones,
    ...      "rho_m": 4.0 * ones},
    ...     grid, Normalization.identity(),
    ... )
    >>> float(compute_field("v_A", ds)[0, 0, 0])  # |B| / sqrt(rho_m)
    2.5
    """
    if _depth > _MAX_DEPTH:
        msg = f"Dependency chain too deep (>{_MAX_DEPTH}) while computing {name!r}"
        raise RecursionError(msg)

    # Check original name first — raw fields take priority over aliases
    if dataset.has_field(name):
        return dataset[name]

    canonical = _resolve_name(name)

    # Direct field lookup after alias resolution
    if dataset.has_field(canonical):
        return dataset[canonical]

    try:
        recipe, result = _execute_recipe(canonical, dataset, _depth)
    except UnknownFieldError as exc:
        if _depth:
            raise
        msg = f"Cannot compute {name!r}: {exc.args[0]}"
        raise UnknownFieldError(msg) from exc

    if recipe.component is not None:
        return result[recipe.component]  # type: ignore[no-any-return]
    return result  # type: ignore[no-any-return]

field_si_factor(name, normalization)

Return the SI conversion factor for a field or derived quantity.

Parameters:

Name Type Description Default
name str

Field or derived quantity name.

required
normalization Normalization

Active normalization.

required

Returns:

Type Description
float

Multiplicative factor: si_value = code_value * factor.

Raises:

Type Description
ValueError

If the quantity type for name is unknown.

Examples:

>>> from pypic.units import Normalization
>>> norm = Normalization.mhd_standard(6.371e6, 1.67e-17, 5.0e-9)
>>> field_si_factor("B_1", norm)  # b_field type resolves to B_ref
5e-09
Source code in src/pypic/compute.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def field_si_factor(name: str, normalization: Normalization) -> float:
    """Return the SI conversion factor for a field or derived quantity.

    Parameters
    ----------
    name : str
        Field or derived quantity name.
    normalization : Normalization
        Active normalization.

    Returns
    -------
    float
        Multiplicative factor: ``si_value = code_value * factor``.

    Raises
    ------
    ValueError
        If the quantity type for *name* is unknown.

    Examples
    --------
    >>> from pypic.units import Normalization
    >>> norm = Normalization.mhd_standard(6.371e6, 1.67e-17, 5.0e-9)
    >>> field_si_factor("B_1", norm)  # b_field type resolves to B_ref
    5e-09
    """
    canonical = _resolve_name(name)
    info = _FIELD_INFO.get(canonical)
    if info is None:
        # Resolve field aliases (Bx→B_1, B_x→B_1, P_e→Pe, etc.)
        fallback = _get_field_alias_fallback()
        canonical = fallback.get(canonical, canonical)
        info = _FIELD_INFO.get(canonical)
    quantity_type: str | None = info.quantity_type if info is not None else None
    if quantity_type is None:
        # Try regex patterns for per-species fields (n_s2, J_s3_1, etc.)
        for pattern, qtype in _SPECIES_QUANTITY_PATTERNS:
            if pattern.match(canonical):
                quantity_type = qtype
                break
    if quantity_type is None:
        msg = (
            f"No SI conversion known for {name!r}. Known fields: {sorted(_FIELD_INFO)}"
        )
        raise ValueError(msg)
    return normalization.si_factor(quantity_type)

display_unit_factor(unit_str)

Return the SI value of a display unit string.

Parameters:

Name Type Description Default
unit_str str

Unit string (e.g. "nT", "km/s").

required

Returns:

Type Description
float

Value of one display unit in SI.

Raises:

Type Description
ValueError

If unit_str is not recognized.

Examples:

>>> display_unit_factor("nT"), display_unit_factor("km/s")
(1e-09, 1000.0)
Source code in src/pypic/compute.py
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def display_unit_factor(unit_str: str) -> float:
    """Return the SI value of a display unit string.

    Parameters
    ----------
    unit_str : str
        Unit string (e.g. ``"nT"``, ``"km/s"``).

    Returns
    -------
    float
        Value of one display unit in SI.

    Raises
    ------
    ValueError
        If *unit_str* is not recognized.

    Examples
    --------
    >>> display_unit_factor("nT"), display_unit_factor("km/s")
    (1e-09, 1000.0)
    """
    try:
        return _DISPLAY_UNITS[unit_str]
    except KeyError:
        valid = sorted(_DISPLAY_UNITS)
        msg = f"Unknown unit {unit_str!r}. Valid: {valid}"
        raise ValueError(msg) from None

available_quantities()

Return sorted list of registered quantity names and aliases.

Does not include dynamically synthesized per-species quantities (e.g. "omega_p_s2", "T_s3"), which are also computable via compute_field.

Returns:

Type Description
list[str]

Examples:

>>> names = available_quantities()
>>> "beta" in names, "v_A" in names, "omega_p_s2" in names
(True, True, False)
Source code in src/pypic/compute.py
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def available_quantities() -> list[str]:
    """Return sorted list of registered quantity names and aliases.

    Does not include dynamically synthesized per-species quantities
    (e.g. ``"omega_p_s2"``, ``"T_s3"``), which are also computable
    via `compute_field`.

    Returns
    -------
    list[str]

    Examples
    --------
    >>> names = available_quantities()
    >>> "beta" in names, "v_A" in names, "omega_p_s2" in names
    (True, True, False)
    """
    return sorted(set(_REGISTRY) | set(COMPUTE_ALIASES))

field_dependencies(name, _depth=0)

Return the raw field names needed to compute name.

Recursively walks the compute recipe graph. If name has no recipe (i.e. it is a raw field), returns {name}.

Parameters:

Name Type Description Default
name str

Field or derived quantity name (e.g. "Pi", "beta").

required

Returns:

Type Description
set[str]

Leaf field names that must be present in the dataset.

Examples:

>>> sorted(field_dependencies("v_A"))
['B_1', 'B_2', 'B_3', 'rho_m']
>>> sorted(field_dependencies("rho_m"))  # a raw field is its own leaf
['rho_m']
Source code in src/pypic/compute.py
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
def field_dependencies(name: str, _depth: int = 0) -> set[str]:
    """Return the raw field names needed to compute *name*.

    Recursively walks the compute recipe graph. If *name* has no recipe
    (i.e. it is a raw field), returns ``{name}``.

    Parameters
    ----------
    name : str
        Field or derived quantity name (e.g. ``"Pi"``, ``"beta"``).

    Returns
    -------
    set[str]
        Leaf field names that must be present in the dataset.

    Examples
    --------
    >>> sorted(field_dependencies("v_A"))
    ['B_1', 'B_2', 'B_3', 'rho_m']
    >>> sorted(field_dependencies("rho_m"))  # a raw field is its own leaf
    ['rho_m']
    """
    if _depth > _MAX_DEPTH:
        msg = f"Dependency chain too deep (>{_MAX_DEPTH}) while resolving {name!r}"
        raise RecursionError(msg)

    canonical = _resolve_name(name)
    try:
        recipe = _get_recipe(canonical)
    except KeyError:
        return {canonical}

    deps: set[str] = set()
    for field in recipe.fields:
        deps |= field_dependencies(field, _depth + 1)
    return deps

compute_with_siblings(name, dataset)

Compute name and, for a vector component, its siblings in one call.

Component recipes (S_1, curl_B_2, V_s2_perp_3) share one tuple-returning function, so evaluating it once yields every component. Scalar recipes return a single entry keyed by name.

Parameters:

Name Type Description Default
name str

Field or derived quantity name (canonical or alias).

required
dataset FieldDataset

Source of the dependency fields.

required

Returns:

Type Description
dict[str, FloatArray]

One entry for a scalar recipe; one per component, keyed by the registry names, for a vector recipe.

Source code in src/pypic/compute.py
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def compute_with_siblings(name: str, dataset: FieldDataset) -> dict[str, FloatArray]:
    r"""Compute *name* and, for a vector component, its siblings in one call.

    Component recipes (``S_1``, ``curl_B_2``, ``V_s2_perp_3``) share one
    tuple-returning function, so evaluating it once yields every
    component. Scalar recipes return a single entry keyed by *name*.

    Parameters
    ----------
    name : str
        Field or derived quantity name (canonical or alias).
    dataset : FieldDataset
        Source of the dependency fields.

    Returns
    -------
    dict[str, FloatArray]
        One entry for a scalar recipe; one per component, keyed by the
        registry names, for a vector recipe.
    """
    siblings = _find_sibling_components(name)
    if not siblings:
        return {name: compute_field(name, dataset)}
    _recipe, full_result = _execute_recipe(_resolve_name(name), dataset)
    return {sibling: full_result[index] for sibling, index in siblings.items()}

register_recipe(name, func, fields, quantity_type, *, needs_grid=False, needs_gamma=False, needs_c=False, long_name='', latex='')

Register a custom derived quantity.

Registers both the computation recipe and the field metadata, so compute(), in_si(), field_info(), and with_derived() all work for the custom field.

Parameters:

Name Type Description Default
name str

Quantity name (e.g. "R_reconnection").

required
func Callable

Pure function: takes arrays (one per field in fields), plus grid spacing if needs_grid, plus gamma if needs_gamma, plus c if needs_c. Returns a single array.

required
fields tuple[str, ...]

Input field names (canonical or derived). Resolved recursively at compute time.

required
quantity_type QuantityType | str

Physical quantity type for SI conversion.

required
needs_grid bool

If True, grid spacing (dx, dy, dz) is appended to args.

False
needs_gamma bool

If True, adiabatic index \(\gamma\) is appended to args.

False
needs_c bool

If True, speed of light \(c\) is appended to args.

False
long_name str

Human-readable label for plot titles.

''
latex str

LaTeX symbol for plot labels.

''

Raises:

Type Description
ValueError

If name already exists in the recipe registry.

Examples:

>>> import numpy as np
>>> register_recipe(
...     "e_mag_ratio",
...     func=lambda eb, ee: eb / (eb + ee),
...     fields=("e_B", "e_E"),
...     quantity_type="dimensionless",
...     long_name="Magnetic-to-total EM energy ratio",
... )
>>> "e_mag_ratio" in available_quantities()
True
>>> unregister_recipe("e_mag_ratio")
Source code in src/pypic/compute.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
def register_recipe(
    name: str,
    func: Callable[..., Any],
    fields: tuple[str, ...],
    quantity_type: QuantityType | str,
    *,
    needs_grid: bool = False,
    needs_gamma: bool = False,
    needs_c: bool = False,
    long_name: str = "",
    latex: str = "",
) -> None:
    r"""Register a custom derived quantity.

    Registers both the computation recipe and the field metadata,
    so ``compute()``, ``in_si()``, ``field_info()``, and
    ``with_derived()`` all work for the custom field.

    Parameters
    ----------
    name : str
        Quantity name (e.g. ``"R_reconnection"``).
    func : Callable
        Pure function: takes arrays (one per field in *fields*),
        plus grid spacing if *needs_grid*, plus gamma if
        *needs_gamma*, plus c if *needs_c*. Returns a single array.
    fields : tuple[str, ...]
        Input field names (canonical or derived). Resolved
        recursively at compute time.
    quantity_type : QuantityType | str
        Physical quantity type for SI conversion.
    needs_grid : bool
        If ``True``, grid spacing ``(dx, dy, dz)`` is appended to args.
    needs_gamma : bool
        If ``True``, adiabatic index $\gamma$ is appended to args.
    needs_c : bool
        If ``True``, speed of light $c$ is appended to args.
    long_name : str
        Human-readable label for plot titles.
    latex : str
        LaTeX symbol for plot labels.

    Raises
    ------
    ValueError
        If *name* already exists in the recipe registry.

    Examples
    --------
    >>> import numpy as np
    >>> register_recipe(
    ...     "e_mag_ratio",
    ...     func=lambda eb, ee: eb / (eb + ee),
    ...     fields=("e_B", "e_E"),
    ...     quantity_type="dimensionless",
    ...     long_name="Magnetic-to-total EM energy ratio",
    ... )
    >>> "e_mag_ratio" in available_quantities()
    True
    >>> unregister_recipe("e_mag_ratio")
    """
    recipe = Recipe(
        func=func,
        fields=fields,
        needs_grid=needs_grid,
        needs_gamma=needs_gamma,
        needs_c=needs_c,
    )
    with _recipe_lock:
        if name in _REGISTRY:
            msg = f"Recipe {name!r} already registered"
            raise ValueError(msg)
        _REGISTRY[name] = recipe

    try:
        register_field(name, quantity_type, long_name=long_name, latex=latex)
    except Exception:
        with _recipe_lock:
            _REGISTRY.pop(name, None)
        raise

unregister_recipe(name)

Remove a custom derived quantity.

Removes both the computation recipe and the field metadata.

Raises:

Type Description
KeyError

If name is not registered.

Source code in src/pypic/compute.py
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
def unregister_recipe(name: str) -> None:
    r"""Remove a custom derived quantity.

    Removes both the computation recipe and the field metadata.

    Raises
    ------
    KeyError
        If *name* is not registered.
    """
    with _recipe_lock:
        try:
            recipe = _REGISTRY.pop(name)
        except KeyError:
            msg = f"No recipe registered for {name!r}"
            raise KeyError(msg) from None

    try:
        unregister_field(name)
    except Exception:
        with _recipe_lock:
            _REGISTRY[name] = recipe
        raise