Skip to content

The @Lie macro ​

The macro @Lie is a thin, readable front end over ad and Poisson. It uses bracket notation that mirrors the mathematics:

  • square brackets [X, Y] denote a Lie bracket of vector fields;

  • curly braces {H, G} denote a Poisson bracket of Hamiltonians.

Brackets may be nested and combined with ordinary arithmetic. Operands may be plain Functions or typed objects. Evaluation points — including 2-element vector literals such as [1.0, 2.0] — may appear directly inside the macro expression.

julia
using CTLie          # Lift, ad, Poisson, ∂ₜ, @Lie
using CTBase: Data   # Data.VectorField, Data.Hamiltonian, …
# AD backend: CTBase's DifferentiationInterface extension + ForwardDiff
using DifferentiationInterface: DifferentiationInterface
using ForwardDiff: ForwardDiff

Lie and Poisson brackets ​

julia
X = Data.VectorField(x -> [x[2], 2x[1]]; is_autonomous=true)
Y = Data.VectorField(x -> [3x[2], -x[1]]; is_autonomous=true)
(@Lie [X, Y])([1.0, 2.0])
2-element Vector{Float64}:
   7.0
 -14.0
julia
F = Data.Hamiltonian((x, p) -> p[1]^2 / 2 + x[1]^2; is_autonomous=true)
G = Data.Hamiltonian((x, p) -> x[1] * p[1]; is_autonomous=true)
(@Lie {F, G})([1.0, 2.0], [0.5, 1.0])
-1.75

The macro returns exactly what the underlying function returns — a typed VectorField for […], a typed Hamiltonian for {…}:

julia
(@Lie [X, Y]) isa Data.VectorField, (@Lie {F, G}) isa Data.Hamiltonian
(true, true)

Nested brackets ​

julia
(@Lie [[X, Y], Y])([1.0, 2.0])
2-element Vector{Float64}:
 -84.0
 -14.0
julia
(@Lie {{F, G}, G})([1.0, 2.0], [0.5, 1.0])
4.5

Evaluation at literal points ​

Vector literals can appear directly as arguments inside a @Lie expression. The macro resolves the ambiguity at runtime through dispatch: if the two elements of a [a, b] are not field-like objects, the macro reconstructs the vector as data and applies it.

julia
@Lie [X, Y]([1.0, 2.0])
2-element Vector{Float64}:
   7.0
 -14.0
julia
h0 = (x, p) -> (2x[1]^2 + x[2]^2 + p[1]^2) / 2
h1 = (x, p) -> (3x[1]^2 + x[2]^2 + p[2]^2) / 2
H0 = Data.Hamiltonian(h0; is_autonomous=true)
H1 = Data.Hamiltonian(h1; is_autonomous=true)
@Lie {H0, H1}([1.0, 2.0], [2.0, 1.0])
4.0

Arithmetic on bracket values ​

Brackets can be evaluated and combined inside a single @Lie expression. With the Bloch-equation fields

julia
F0 = Data.VectorField(x -> [-2x[1], -2x[2], 1 - x[3]]; is_autonomous=true)
F1 = Data.VectorField(x -> [0.0, -x[3], x[2]];         is_autonomous=true)
F2 = Data.VectorField(x -> [x[3], 0.0, -x[1]];         is_autonomous=true)
x3 = [1.0, 2.0, 3.0]

we have     and   , so

julia
@Lie [F0, F1](x3) + 4 * [F1, F2](x3)
3-element Vector{Float64}:
  8.0
 -8.0
 -2.0

The same works for Poisson brackets:

julia
h2 = (x, p) -> (4x[1]^2 + x[2]^2 + p[1]^3 + p[2]^2) / 2
H2 = Data.Hamiltonian(h2; is_autonomous=true)
x0, p0 = [1.0, 2.0], [2.0, 1.0]
@Lie {H0, H1}(x0, p0) - {H1, H2}(x0, p0)
22.0

Trailing keywords bind to @Lie

A macro greedily consumes the keyword=value arguments that follow it. So in @Lie [F, G](x) ≈ v atol=1e-6, the atol=1e-6 is handed to @Lie (and rejected), not to ≈. Wrap the bracket expression in parentheses to scope the macro:

julia
(@Lie [F, G](x)) ≈ v atol=1e-6   # atol now belongs to the comparison

Plain functions and trait keywords ​

The operands may be plain Functions. As elsewhere, a bare function is assumed autonomous and fixed; use the keyword flags is_autonomous and is_variable to say otherwise. They are passed after the bracket expression:

julia
f = x -> [x[2], 2x[1]]
g = x -> [3x[2], -x[1]]
(@Lie [f, g])([1.0, 2.0])             # autonomous by default
2-element Vector{Float64}:
   7.0
 -14.0
julia
ft = (t, x) -> [t + x[2], -2x[1]]
gt = (t, x) -> [t + 3x[2], -x[1]]
(@Lie [ft, gt] is_autonomous=false)(1.0, [1.0, 2.0])
2-element Vector{Float64}:
 -5.0
 11.0
julia
fv = (x, v) -> [x[2] + v, 2x[1]]
gv = (x, v) -> [3x[2], v - x[1]]
(@Lie [fv, gv] is_variable=true)([1.0, 2.0], 1.0)
2-element Vector{Float64}:
   6.0
 -15.0

A third keyword, ad_backend, selects the AD backend for this call only (see Limitations & configuration).

When operands are typed, their traits are read directly and no keyword is needed — indeed, a keyword that contradicts the operands' traits is an error (next section).

Error cases ​

The macro validates its input. The errors below are captured when the documentation is built.

Wrong bracket kind for a typed operand — a Hamiltonian inside [...] (Lie bracket) or a VectorField inside {...} (Poisson bracket). The operand itself is wrong for that bracket, so this raises CTBase.Exceptions.IncorrectArgument:

julia> H = Data.Hamiltonian((x, p) -> p[1] * x[1]);

julia> F = Data.VectorField(x -> [x[2], -x[1]]);

julia> @Lie [H, F]
ERROR: IncorrectArgument
│
│  @Lie: AbstractHamiltonian cannot be a Lie bracket operand
│
│  Context  @Lie macro runtime dispatch
│  Hint     Use {H, G} (Poisson bracket) for Hamiltonians
└─

julia> @Lie [F, H]
ERROR: IncorrectArgument
│
│  @Lie: AbstractHamiltonian cannot be a Lie bracket operand
│
│  Context  @Lie macro runtime dispatch
│  Hint     Use {H, G} (Poisson bracket) for Hamiltonians
└─

julia> @Lie {F, H}
ERROR: IncorrectArgument
│
│  @Lie: AbstractVectorField cannot be a Poisson bracket operand
│
│  Context  @Lie macro runtime dispatch
│  Hint     Use [X, Y] (Lie bracket) for VectorFields
└─

julia> @Lie {H, F}
ERROR: IncorrectArgument
│
│  @Lie: AbstractVectorField cannot be a Poisson bracket operand
│
│  Context  @Lie macro runtime dispatch
│  Hint     Use [X, Y] (Lie bracket) for VectorFields
└─

An unknown keyword argument also raises IncorrectArgument:

julia> @Lie [F, F] invalid_arg=true
ERROR: IncorrectArgument
│
│  @Lie: unknown keyword argument
│
│  Got       invalid_arg
│  Expected  is_autonomous, is_variable, or ad_backend
│
│  Context   @Lie macro keyword parsing
└─

Operands whose traits disagree, or a keyword that contradicts the operands' traits. Each piece is valid on its own — it is their combination that is rejected — so these raise CTBase.Exceptions.PreconditionError:

julia> Xa = Data.VectorField(x -> [x[2], -x[1]]; is_autonomous=true);

julia> Xt = Data.VectorField((t, x) -> [x[2], -x[1]]; is_autonomous=false);

julia> @Lie [Xa, Xt]
ERROR: PreconditionError
│
│  ad: TD/VD mismatch between X and Y
│
│  Reason   X: CTBase.Traits.Autonomous/CTBase.Traits.Fixed ≠ Y: CTBase.Traits.NonAutonomous/CTBase.Traits.Fixed — both arguments must share the same TimeDependence and VariableDependence
│
│  Context  ad on AbstractVectorField
│  Hint     Ensure both vector fields have the same time and variable dependence traits
└─

julia> @Lie [Xa, Xa] is_autonomous=false
ERROR: PreconditionError
│
│  @Lie: is_autonomous conflicts with operand trait
│
│  Reason   `is_autonomous` specifies CTBase.Traits.NonAutonomous but operand has CTBase.Traits.Autonomous
│
│  Context  @Lie consistency check
└─

How the macro works ​

@Lie is a macro-time rewrite: every [a, b] in the expression is replaced by a call to the runtime worker _lie_mac(a, b, …), and every {c, d} by _poisson_mac(c, d, …). The bracket kind and the trait arguments are baked in at expansion time. Whether [a, b] is a genuine Lie bracket or a data vector is resolved at runtime through multiple dispatch:

  • Both operands are field-like (Function or AbstractVectorField) → Lie bracket.

  • Either operand is an AbstractHamiltonian → IncorrectArgument.

  • Otherwise → the two values are reassembled as [a, b] (data vector).

This means the macro is not type-stable when it encounters a data vector [a, b]: the return type depends on the runtime values of a and b. When both operands are known to be field-like (e.g. typed VectorField objects), the macro is type-stable.

See also ​