User functions

A fit function Asymmetry doesn’t ship is one function away. Every fit component and parameter-trend model is backed by an ordinary Python file under ~/.asymmetry/user_functions/ — at the next start it appears in the fit-function builder (under User, badged · user), fits like any built-in component, exports through GLE plot labels, and survives project save/load. No rebuild, no packaging, no changes to Asymmetry itself — extending the fitting library is plain Python.

You write that file yourself only if you want to; the usual route is to let the GUI write it for you.

Building a function in the GUI

New User Function dialog authoring a stretched-exponential oscillation

The New User Function dialog mid-authoring: StretchedOsc, a stretched-exponential envelope on a Larmor oscillation. The parameter table carries a start value per name; the preview redraws at those values, and the green status line with an enabled OK button confirms every load-time check has passed.

Both function builders — the fit builder (Fit panel → Build…) and the parameter-model builder (the parameter-trending window’s model editor) — have a New user function… button. It sits in the library footer, and a second copy appears as an invitation whenever a search comes up with no matches, so typing the name of a function that doesn’t exist yet is itself a route to creating it. Either button opens the same dialog.

The dialog asks for:

  • Name — the identifier the function is registered and inserted under (e.g. StretchedOsc); it must be unique across every built-in and user-defined component.

  • Description — the one-line summary shown in the library and the component info dialog.

  • Formula — a maths expression in x (time in μs, frequency in MHz, or the trend variable, depending on where you opened the dialog from) and your parameter names. Bare maths names (exp, sin, cos, sqrt, pi, …) and np.* both work, so A*exp(-lam*x) and A*np.exp(-lam*x) are equivalent.

  • Parameters — a small table of parameter names and start values. Click Detect parameters to populate it automatically from every name in the formula that isn’t x, np, or a recognised maths function; by convention the first parameter of a fit component is its amplitude.

  • Edit as Python (advanced) — a toggle that replaces the single-line formula with a full editor, pre-filled with the exact code your formula would have generated. Use it for anything a one-line expression can’t express — conditionals, piecewise definitions, a helper computation before the return.

As you type, a preview curve redraws at the parameter start values, so a typo or a domain mismatch shows up as a wrong-looking curve (or a validation message) before you commit to anything. Validation is debounced — it waits until you pause rather than running on every keystroke, because building the preview curve executes your code. It runs exactly the checks a hand-written plugin faces at load time — a legal, globally unique identifier, valid parameter names (none clashing with x, np, or a maths function), a non-empty description, and a finite result on a probe grid — with no second, looser path; OK stays disabled until they all pass.

Press OK and Asymmetry writes an ordinary, readable plugin file into ~/.asymmetry/user_functions/ (the same folder and format described below — hand-editable, and it reloads at every subsequent startup exactly like a plugin you wrote yourself), registers the function immediately, and inserts it into the model you were building. There is no extra “install” step and no restart needed for the function you just created; Setup → User Functions… lists it alongside every other loaded plugin from that moment on.

Note

Whichever route creates it, a user function is ordinary Python executed with full interpreter privileges. Only create or install functions you trust, and be as careful sharing a generated plugin file as you would sharing a hand-written one.

Writing the file yourself

The GUI dialog covers a single formula in x. For anything that benefits from being scripted, versioned, or shared as a package — a model with helper functions, one you want under source control, or one you want to distribute to a group — write the plugin file directly. This is the same mechanism the GUI uses under the bonnet, so a function created either way is indistinguishable to the rest of Asymmetry.

The worked example re-implements the shipped Keren component — the analytic dynamic Gaussian relaxation in a longitudinal field [1] — and is verified in the test suite to match it bit for bit. Replace the body and metadata with your own physics:

"""Keren dynamic relaxation as an Asymmetry user function.

Drop this file in ``~/.asymmetry/user_functions/`` and restart Asymmetry:
``KerenUser`` appears in the fit-function builder under *User*, fits like
any built-in component, and survives project save/load.

The function reproduces the shipped ``Keren`` component exactly (it is the
worked example from the "User functions" chapter of the user guide) — for
your own physics, replace the body and metadata.
"""

import numpy as np

from asymmetry import register_component
from asymmetry.core.utils.constants import (
    GAUSS_TO_TESLA,
    MUON_GYROMAGNETIC_RATIO_MHZ_PER_T,
)


def keren_user(t, A, Delta, nu, B_L):
    """Keren's analytic dynamic Gaussian relaxation in a longitudinal field.

    P(t) = A exp[-Gamma(t)] with omega_0 = gamma_mu B_L and

        Gamma(t) = (2 Delta^2 / (omega_0^2 + nu^2)^2) * [
            (omega_0^2 + nu^2) nu t
            + (omega_0^2 - nu^2) (1 - e^{-nu t} cos(omega_0 t))
            - 2 nu omega_0 e^{-nu t} sin(omega_0 t) ]

    after A. Keren, Phys. Rev. B 50, 10039 (1994). At B_L = 0 it reduces to
    the Abragam exponent; as nu and B_L both vanish it tends to the static
    Gaussian envelope exp(-Delta^2 t^2).
    """
    t = np.asarray(t, dtype=float)
    gamma_mu = 2.0 * np.pi * MUON_GYROMAGNETIC_RATIO_MHZ_PER_T
    omega0 = gamma_mu * (float(B_L) * GAUSS_TO_TESLA)  # rad/us
    delta2 = float(Delta) * float(Delta)
    w2 = omega0 * omega0
    n2 = float(nu) * float(nu)
    denom = w2 + n2

    if denom < 1e-20:
        # nu -> 0 and B_L -> 0: Gamma -> Delta^2 t^2 (static Gaussian limit)
        exponent = np.clip(-delta2 * t * t, -700, 0)
        return A * np.exp(exponent)

    e = np.exp(np.clip(-float(nu) * np.abs(t), -700, 0))
    gamma = (2.0 * delta2 / (denom * denom)) * (
        denom * float(nu) * np.abs(t)
        + (w2 - n2) * (1.0 - e * np.cos(omega0 * t))
        - 2.0 * float(nu) * omega0 * e * np.sin(omega0 * t)
    )
    exponent = np.clip(-gamma, -700, 0)
    return A * np.exp(exponent)


register_component(
    "KerenUser",
    keren_user,
    ["A", "Delta", "nu", "B_L"],
    domain="time",
    description=(
        "Keren dynamic Gaussian relaxation in a longitudinal field "
        "(user-function copy of the built-in Keren component)"
    ),
    formula_template="{A}*exp(-Gamma(t; Delta={Delta}, nu={nu}, B_L={B_L}))",
    latex_equation=(
        r"A(t)=A\exp[-\Gamma(t)],\ \Gamma(t)=\frac{2\Delta^2}{(\omega_0^2+\nu^2)^2}"
        r"\left[(\omega_0^2+\nu^2)\nu t+(\omega_0^2-\nu^2)(1-e^{-\nu t}\cos\omega_0 t)"
        r"-2\nu\omega_0 e^{-\nu t}\sin\omega_0 t\right],\ \omega_0=\gamma_\mu B_L"
    ),
    applicability=(
        "Use for dynamically fluctuating Gaussian local fields in an applied "
        "longitudinal field, where the fluctuation rate ν and the Larmor "
        "frequency ω₀ are comparable — the analytic fast-fluctuation "
        "alternative to the numerical dynamic Kubo-Toyabe function."
    ),
    references=("A. Keren, Phys. Rev. B 50, 10039 (1994).",),
    param_defaults={"A": 25.0, "Delta": 0.5, "nu": 1.0, "B_L": 0.0},
)

Three steps:

  1. Save the file in ~/.asymmetry/user_functions/ (create the folder if it doesn’t exist; any name ending in .py works, names starting with _ are skipped).

  2. Restart Asymmetry. The log panel reports N user function(s) registered; Setup → User Functions… shows the full load report at any time.

  3. Open the fit-function builder — KerenUser is in the User submenu. Build KerenUser + Constant, press Fit, and read off Δ and ν as usual, e.g. Δ = 0.51(2) μs⁻¹ at 20 G.

The function contract

register_component(name, function, param_names, *, domain, description, formula_template, ...) validates everything at load time — a broken file can never crash a fit (or the application) later:

  • function(x, **params) must be vectorised: x is an ndarray (time in μs for domain="time", frequency in MHz for "frequency"), one keyword argument per entry of param_names, ndarray of the same shape back.

  • The output must be finite on a probe grid at the default parameter values (NaN/Inf is rejected with a message naming the file).

  • The name must be a bare identifier usable in builder expressions, and must be unique across all of Asymmetry’s function registries — that is why the example is KerenUser, not Keren.

  • Optional physics tags feed the fit wizard’s scope selector: field_geometries (any of "ZF", "TF", "LF"; default all three), physics_classes (e.g. "magnetism", "dynamics", "muonium"; default "custom"), and cost ("cheap", "moderate", or "expensive"; default "moderate"). Untagged user functions match every wizard scope preset, so they are never hidden; tagging them narrows when the wizard auto-considers them and tells the tiered screener how expensive they are.

  • domain is required; it places the component in the matching picker and plots. Optional metadata (latex_equation, applicability, references, category, fixed_params, param_defaults) gives the component the same info-dialog documentation as a built-in.

Parameter-trend components — functions of temperature or field for the parameter-trending builder, including the ⊕ quadrature grammar — register through the sibling asymmetry.register_parameter_component(), whose scopes argument ("temperature", "field", "common") controls where the component is offered.

Failures, scripts, and sharing

Anything that goes wrong — a syntax error, a failed validation, a name collision — is confined to that file: the rest of your plugins and the application load normally, the log panel carries one line per failure, and Setup → User Functions… shows the full error text. Fix the file and restart (files are imported once at startup; there is no hot reload).

In analysis scripts, load your plugin directory explicitly:

import asymmetry
asymmetry.load_user_functions()          # ~/.asymmetry/user_functions
# or: asymmetry.register_component(...)  # register directly, no file

To share functions as an installable package, expose a callable that performs the registrations under the asymmetry.user_functions entry-point group; installed packages load automatically at startup.

A project that references a user function which is not installed (a colleague’s .asymp, or your own after removing a plugin) opens with the model intact: the missing component is shown by name, plots as zero, and fitting is blocked with a message saying which function to restore. Saving the project preserves the original model unchanged.

Removing a function is the reverse of installing one: delete its file from the plugin folder — Setup → User Functions…Open folder… takes you straight there — and restart. Saved projects that referenced it degrade exactly as above, to the named placeholder, and the function goes live again if the file is ever restored.

User functions are ordinary Python executed with full interpreter privileges — only install files you trust.