Evaluating expressions

A PALS lattice may write numeric values as mathematical expressions0.3 * r_electron, a_const^2, mass_of("proton"). As part of expansion, parse_and_expand_PALS evaluates every such expression to a plain number, so both expanded trees are fully numeric. The original and combined views always keep the expression text exactly as written.

The expression language itself — the grammar and its precedence, the built-in constants, the functions, the syntax for defining constants and variables, for Controller elements and for set commands — belongs to the PALS standard and is documented with it at pals-project.readthedocs.io. This page covers what this library does with it: when evaluation happens, what it declines to evaluate, and what it reports rather than guesses.

Constant values come from AtomicAndPhysicalConstantsCLib (APC — a C++ mirror of AtomicAndPhysicalConstants.jl, CODATA 2022), fetched automatically by CMake, so pals-cpp shares one set of numbers with the rest of the toolchain.

Where the standard leaves a choice

Two points the standard’s function and constant lists do not settle, which this library therefore has to fix:

  • Associativity and the unary sign. ^ is right-associative, so 2^3^2 is 2^(3^2) = 512. A unary sign binds looser than ^, so -2^2 is -(2^2) = -4, while a signed exponent still works (2^-2 is 0.25). This is the Fortran/Bmad convention used across the ecosystem.

  • Species names must be quoted. mass_of("proton") is read; an unquoted name is an error. A mass number carries a leading #"#3He", not "3He" — matching AtomicAndPhysicalConstants.jl, and quoting is also what lets that # be written without tripping YAML’s comment rule. The argument may instead be a constant or variable whose value is a species name, passed unquoted; such a definition may also be used directly wherever a species name is expected, and a bare identifier used as a parameter value is replaced by the species string it stands for.

What gets evaluated

Only scalars that are genuine expressions are touched. Names that happen to sit where a value could go — element and line references in a line:, kind: names, booleans, a MetaP description — are not expressions and are left alone. Immediate expressions (length: 0.3 * r_electron) and delayed ones (Kn1: expr(3.74 * a_var)) both become numbers; in the expanded tree the distinction no longer matters.

A value that fails to evaluate is left as text rather than aborting the expansion, and it is reported as a problem only if it looks like one — an operator, a parenthesis, a > reference, or an explicit expr(...). That is what keeps a plain name or a label from being mistaken for broken arithmetic.

Some keys are excluded from evaluation outright, because their values are names or free-form prose that could otherwise collide with a constant or parse as arithmetic — kind, use and inherit, the fork targets, and the authors, notes and reminders prose among them.

Definitions are resolved in dependency order, so a value may reference one written later in the file as readily as one written earlier. A reference that cannot be resolved, or a genuine cycle, leaves the value as text and is reported.

Randomness is deferred, never invented

An expression that calls random() or random_gauss() is left as text, so that expanding the same lattice twice gives byte-identical output:

Kn2: 0.01 + 0.003*random_gauss()   # kept verbatim

The same rule reaches everywhere such an expression can appear: a control expression containing one is deferred and so drives nothing, and a set whose value calls one leaves its parameter untouched.

absolute_error and relative_error follow from the same principle. The standard gives the error magnitudeabsolute_error + relative_error * |value| — but not its distribution, so this library reads them, writes the deterministic value, and reports a problem rather than picking a distribution on the author’s behalf.

How sets are applied

A parameter of a known element that has not been written reads as zero, so PARAMETER + 0.02 works on an element that carries no such group yet — the group is created. The exception is a parameter that is still to be derived: if any member of its linked family has been written (the four interchangeable forms BnN/BnNL/KnN/KnNL of a magnetic multipole component, the two forms of an electric one, or the tied BendP geometry), its value is the bookkeeper’s to compute and reading it beforehand is an error:

- Q1:
    kind: Quadrupole
    MagneticMultipoleP:
      Ks1: 0.34
- set:
    parameter: Q2>MagneticMultipoleP.Bs1
    value: Q1>MagneticMultipoleP.Bs1   # error: Bs1 needs the reference momentum

expand_lattice and where a set acts

An expand_lattice node divides the facility list in two, and which side a set sits on decides what it writes:

  • Before expand_lattice a set acts on the element definitions, and only on those defined earlier in the list — an element defined after it is untouched. One definition is written once, so every copy of a repeated element inherits the same value.

  • After expand_lattice a set acts on the already-expanded lattice, so each copy is a separate element and is written separately. The bookkeeper has already run at this point, so a value may use computed parameters such as SELF.s_position.

The order of the passes

pre-expansion sets
  → branch and fork expansion
  → expression evaluation
  → ABSOLUTE controllers
  → element bookkeeper
  → post-expansion sets
  → expression evaluation
  → ABSOLUTE controllers again
  → element bookkeeper again

Applying the controllers last is what keeps a controller authoritative over a parameter a later set also writes. The second bookkeeper pass recomputes what those writes invalidated: the reference, floor and position values, and the family members a post-expansion set dropped.

The snapshot that separates expanded from full_expanded is taken between the first expression evaluation and the first bookkeeper pass — see The five trees.

Evaluating a single expression

evaluate_pals_expression evaluates one expression string on its own and returns a double, setting an optional ok flag:

bool ok = false;
double v = evaluate_pals_expression("3.75e7 / c_light^2", &ok);
// ok == true, v == 4.172…e-10

This evaluates a standalone string, so user-defined constants and variables are not in scope — use parse_and_expand_PALS for whole-lattice evaluation, whose expanded trees already have every expression resolved. ok is set to false on a parse error, an unknown identifier or species, an unquoted species name, a random()/random_gauss() expression (intentionally deferred), or a non-finite result.