Implementation details
This page covers performance considerations, caching mechanisms, precompilation, and other implementation details of Semisimple.jl.
Caching
Semisimple.jl uses several internal caches to avoid recomputing expensive results. Understanding these caches is important for benchmarking and memory management.
Available caches
Semisimple.jl maintains ten internal caches. Six are unbounded Dict caches for small singletons and lookup tables; four are bounded LRU caches (from LRUCache.jl) whose total memory budget is configurable at runtime via configure_caches!.
| Cache | Variable | Type | Purpose |
|---|---|---|---|
| Root system | Semisimple._root_system_cache | Dict | Singleton RootSystem instances per Dynkin type |
| Positive roots set | Semisimple._positive_roots_set_cache | Dict | Fast is_positive_root lookup sets |
| Longest Weyl element | Semisimple._longest_element_cache | Dict | Cached longest element w₀ per Dynkin type |
| Coset representatives | Semisimple._coset_reps_cache | Dict | Weyl orbit coset reps for exceptional types |
| Dominant character (type) | Semisimple._dominant_character_type_cache | Dict | Type-level Freudenthal intermediates |
| Weyl dimension data | Semisimple._weyl_dimension_data_cache | Dict | Dimension formula denominator and scaled roots |
| Dominant character | Semisimple._dominant_character_cache | LRU | Dominant weight multiplicities from Freudenthal's formula |
| Tensor product | Semisimple._tensor_cache | LRU | Tensor product decompositions |
| Symmetric power | Semisimple._symmetric_power_cache | LRU | Symmetric power decompositions |
| Exterior power | Semisimple._exterior_power_cache | LRU | Exterior power decompositions |
The six Dict caches are unbounded and persist for the lifetime of the Julia session. The four LRU caches have a configurable memory budget (default: 25 % of system RAM, minimum 256 MiB) and automatically evict least-recently-used entries when the budget is exceeded.
The dominant character cache is the main performance lever for downstream operations. Tensor products, symmetric/exterior powers, and plethysms call dominant_character repeatedly for the same highest weights.
Inspecting caches
Use cache_info to get a snapshot of cache occupancy:
using Semisimple
# Snapshot before any work
info = cache_info()
println("Tensor cache: ", info.tensor.length, " entries (max ", info.tensor.maxsize, " bytes)")
# Populate some caches by doing computations
ω₁ = fundamental_weight(TypeE{8}, 1)
freudenthal_formula(ω₁)
tensor_product(ω₁, ω₁)
# Snapshot after
info = cache_info()
println("Dominant character cache: ", info.dominant_character.length, " entries")
println("Tensor cache: ", info.tensor.length, " entries")Clearing caches
Use clear_caches! (or its alias clear_all_caches!) to empty every cache at once:
using Semisimple
# Do some computations
ω₁ = fundamental_weight(TypeA{2}, 1)
tensor_product(ω₁, ω₁)
freudenthal_formula(ω₁)
symmetric_power(ω₁, 3)
# Clear everything
clear_caches!()This is particularly useful for:
- Benchmarking cold-start performance — measure how long operations take without cached results
- Memory management — free memory after large computations (e.g., after computing many E₈ tensor products)
- Reproducible testing — ensure tests start from a clean state
Individual cache variables have underscored names and are internal implementation details. Prefer the public clear_caches! and configure_caches! APIs.
Configuring cache budgets
Use configure_caches! to resize the LRU caches at runtime. The budget (in bytes) controls the total memory envelope; the four fraction arguments determine how it is divided:
using Semisimple
# Give caches 512 MiB total
configure_caches!(budget = 512 * 1024^2)
# Custom split: 50 % tensor, 30 % dominant, 10 % each for Sym/⋀
configure_caches!(
budget = 512 * 1024^2,
dominant_frac = 0.30,
tensor_frac = 0.50,
sym_power_frac = 0.10,
ext_power_frac = 0.10,
)Default fractions: dominant 30 %, tensor 40 %, symmetric 15 %, exterior 15 %. The default total budget is 25 % of system RAM (minimum 256 MiB). These defaults can also be set persistently via Julia's Preferences.jl (keys: cache_budget, dominant_frac, tensor_frac, sym_power_frac, ext_power_frac).
Cache invalidation
Caches are never invalidated by code changes — all cached functions are pure (same inputs always produce same outputs). However, cached entries can disappear in three ways:
- You explicitly clear a cache (via
clear_caches!orempty!(...)) - An LRU cache evicts least-recently-used entries when its memory budget is exceeded
- Your Julia session ends
Automatic eviction only affects the four bounded LRU caches. The six unbounded Dict caches persist until cleared or session end.
This design is safe because:
- Dynkin types are immutable compile-time constants
- Weights are immutable
SVectorobjects - All cached functions are pure — re-computing an evicted entry always gives the same result
Precompilation
Semisimple.jl precompiles many commonly-used methods to reduce first-call latency. When you load the package with using Semisimple, the precompilation work has already been done.
What gets precompiled
The package precompiles the following operations for all simple Dynkin types up to rank 10 (plus the exceptional types):
Dynkin types precompiled:
TypeA{1}throughTypeA{10}TypeB{2}throughTypeB{10}TypeC{2}throughTypeC{10}TypeD{3}throughTypeD{10}TypeE{6},TypeE{7},TypeE{8}TypeF4TypeG2
Operations precompiled:
cartan_matrix,cartan_symmetrizer,cartan_bilinear_form,cartan_matrix_inverse_make_root_system(internal root system construction)_weyl_denominator,_weyl_dim_scaled_roots(Weyl dimension formula internals)degree(representation dimension)conjugate_dominant_weight(dominant weight conjugation)conjugate_dominant_weight_with_length(Borel–Weil–Bott hot path)weyl_orbit(Weyl orbit generation)- Weyl group actions (
*operator for roots and weights with Weyl elements) freudenthal_formula(weight multiplicities)dot_reduce(weight normalization)tensor_product(Brauer–Klimyk; Littlewood–Richardson rule for Type A)
Keeping precompilation affordable
Covering 41 Dynkin types would be very expensive if every method were compiled separately per type. Instead, the heavy numeric kernels (the Freudenthal recursion, the Weyl dimension formula, dominant-weight enumeration, and the Weyl-orbit traversal) are parametrized by the rank only, so for example A₇, B₇, C₇, D₇, and E₇ all share a single compiled kernel. Only thin per-type wrappers remain, which keeps both precompilation time and the package-image size in check.
To skip the precompile workload entirely (e.g. during development or in CI jobs that never call into the numeric routines), set the package preference:
using Preferences, Semisimple
set_preferences!(Semisimple, "precompile_workload" => false)The package then precompiles in a couple of seconds; all functionality remains available, with compilation happening lazily at first use.
Why precompilation matters
Without precompilation, the first call to a method triggers just-in-time (JIT) compilation, which can take hundreds of milliseconds. With precompilation, these methods are ready to use immediately:
using Semisimple
# First call is fast due to precompilation
@time degree(fundamental_weight(TypeE{8}, 1)) # ~0.001s
# Without precompilation, this would take ~0.5s for the first callWhat is NOT precompiled
Operations involving:
- Product Dynkin types (e.g.,
ProductDynkinType{Tuple{TypeA{2}, TypeB{3}}}) - Rank ≥ 11 simple types (e.g.,
TypeA{15})
These will experience first-call latency but will be fast on subsequent calls (after JIT compilation). Because the numeric kernels are shared per rank, the first-call latency for an uncovered type is limited to its thin wrappers.
Performance characteristics
Compile-time vs. run-time
Semisimple.jl leverages Julia's type system and @generated functions to move selected computations to compile time, while larger derived data is computed once at runtime and cached per Dynkin type (keeping precompilation cheap):
| Compile-Time (Type-Level) | Run-Time (cached per type) | Run-Time |
|---|---|---|
| Dynkin type classification | Root system enumeration | Weight coordinate values |
| Rank of Dynkin type | Weyl denominator product | Weight lattice arithmetic |
| Cartan matrix entries | Reflection tables | Weyl orbit traversal |
| Cartan symmetrizer, bilinear forms | Coset representatives | Freudenthal recursion |
| Character multiplication |
This means that cartan_matrix(TypeE{8}) produces a compile-time constant SMatrix that is embedded directly into your compiled code — there's no matrix allocation at runtime. Root systems, by contrast, are built by a single shared routine on first use and memoized, so the construction cost (microseconds) is paid once per session.
Memory usage
| Operation | Memory Footprint | Notes |
|---|---|---|
RootSystem{TypeE{8}} | ~15 KB | Singleton, cached per type |
WeightLatticeElem | 8R bytes | R = rank; stored as SVector{R,Int} |
WeylGroupElem | ~40 + L bytes | Word stored as Vector{UInt8}; L = word length |
WeylCharacter | ~24 + 40N bytes | N = number of terms in the character |
| Freudenthal cache (E₈ adjoint) | ~40 KB | 3,875 weight multiplicities |
For large-scale computations (e.g., thousands of E₈ tensor products), the character-related LRU caches will automatically evict old entries once their memory budget is reached. Use configure_caches! to increase the budget, or clear_caches! to free memory immediately.
Asymptotic complexity
| Operation | Time Complexity | Notes |
|---|---|---|
degree(λ) | O(N²) | N = number of positive roots |
freudenthal_formula(λ) | O(M·N) | M = |
tensor_product(λ, μ) (BK) | O(M·W·d) | W = Weyl group order, d = dim V(smaller weight) |
tensor_product(λ, μ) (LR, Type A) | O(n³) | n = max( |
symmetric_power(λ, k) | O(k²·T) | T = cost of one tensor product |
weyl_orbit(λ) | O(W·R·R) | W = orbit size ≤ Weyl order, R = rank |
For reproducible performance measurements, see the benchmark scripts in benchmark/.
Type stability
Semisimple.jl is designed for complete type stability:
using Semisimple
ω₁ = fundamental_weight(TypeE{8}, 1)
typeof(ω₁) # WeightLatticeElem{TypeE{8}, 8} — concrete type
ch = freudenthal_formula(ω₁)
typeof(ch) # Dict{SVector{8, Int64}, BigInt} — concrete type
result = tensor_product(ω₁, ω₁)
typeof(result) # WeylCharacter{TypeE{8}, 8} — concrete typeAll public APIs return concrete types, enabling aggressive compiler optimizations. There are no type instabilities in hot paths.
Numerical precision
All computations use exact integer arithmetic — there are no floating-point operations:
- Weights are
SVector{R, Int}— exact integer vectors - Irreducible multiplicities in a
WeylCharacterareInt; weight multiplicities from the Freudenthal recursion areBigInt(they can exceedtypemax(Int64)for large representations) - Dimensions are computed exactly (the Weyl dimension formula works in
BigInt) - Inner products use scaled integer forms to avoid division
This means:
- No numerical stability concerns — safe for arbitrarily large representations
- No overflow — dimensions and weight multiplicities are
BigIntthroughout
Example: the irreducible representation of E₈ with highest weight ρ has dimension $2^{120}$, far beyond typemax(Int64) ≈ 9.2 × 10^{18}:
julia> using Semisimple
julia> ρ = weyl_vector(TypeE{8});
julia> degree(ρ)
1329227995784915872903807060280344576
julia> degree(ρ) == big(2)^120
trueThread safety
The small Dict singleton/type-data caches are protected by locks where they are populated, except that lookups do not take the lock — so a reader racing a first-time writer is unsynchronized. The bounded LRU character caches are internally locked by LRUCache.jl, so concurrent cache-populating calls such as dominant_character or tensor_product cannot corrupt them; at worst two threads compute the same uncached entry twice. Cached dictionaries are returned by reference and must not be mutated.
Safe: Using Semisimple.jl from a single thread (the default)
Safe: Read-only operations from multiple threads after warming up caches
Unsafe: Calling clear_caches! or configure_caches! while other threads are computing
The recommended pattern for parallel computation remains: populate caches in a single-threaded warm-up phase, then perform read-only operations in parallel.
Comparison with LiE
Semisimple.jl reimplements many algorithms from the LiE computer algebra system. Key differences:
| Aspect | LiE (C) | Semisimple.jl (Julia) |
|---|---|---|
| Language | C (CWEB literate programming) | Julia (pure Julia) |
| Type system | Runtime group structs | Compile-time Dynkin type parameters |
| Cartan matrices | Runtime matrix allocation | Compile-time SMatrix constants |
| Caching | Permanent "long-life" objects | Bounded LRU caches + Dict singletons |
| Hot performance | Fast (compiled C) | Fast (JIT-compiled, with caching) |
| Cold performance | Instant (no compilation) | Slow first call (JIT overhead) |
For hot operations (cached, precompiled), Semisimple.jl matches or exceeds LiE's performance. For cold operations, LiE is faster due to no JIT compilation delay.
Implementation philosophy
Semisimple.jl follows these design principles:
- Type-level dispatch at the API surface — Use Julia's type system to specialize the user-facing layer for each Dynkin type, while the heavy numeric kernels are shared per rank so they are compiled only once for e.g. A₇/B₇/C₇/D₇/E₇
- Compile-time constants where they pay —
@generatedCartan data; larger derived data (root systems, Weyl dimension data) is computed once at runtime and memoized - Immutability — All core types are immutable for thread safety and optimization
- Caching — Trade memory for speed by memoizing expensive computations
- Minimal dependencies — StaticArrays.jl, LRUCache.jl, PrecompileTools.jl, Preferences.jl, and LinearAlgebra (stdlib)
- Pure Julia — No C/Fortran, enabling introspection and compilation to other targets
These principles enable aggressive compiler optimizations while maintaining mathematical rigor.
Weyl orbit traversal
Weyl orbits are computed by the internal module Weylloop.jl, which implements LiE-style systematic orbit traversal. Rather than a hash-set BFS that scales with orbit size, it converts weight coordinates to the ε-basis where classical Weyl subgroups act as permutations (type A) or permutations + sign flips (types B/C/D). Orbits are enumerated via lexicographic permutation generation and Gray-code sign flips, eliminating the $O(|\text{orbit}|)$ hash-set overhead that would otherwise dominate for large orbits (e.g., E₈ orbits with millions of elements). For exceptional types, precomputed coset representatives reduce the problem to the classical case.
API reference
Semisimple.clear_all_caches! — Function
clear_all_caches!()Clear all internal caches used by Semisimple.jl. Alias for clear_caches!.
Examples
julia> using Semisimple
julia> clear_all_caches!()Semisimple.clear_caches! — Function
clear_caches!()Empty every internal cache in Semisimple.jl (both bounded Dict caches and LRU caches).
Examples
julia> using Semisimple
julia> ω1 = fundamental_weight(TypeA{2}, 1);
julia> tensor_product(ω1, ω1); # populates caches
julia> clear_caches!()Semisimple.configure_caches! — Function
configure_caches!(; budget=nothing, dominant_frac=nothing, tensor_frac=nothing,
sym_power_frac=nothing, ext_power_frac=nothing)Resize the LRU caches at runtime. Unspecified keyword arguments retain their current values. The budget is in bytes and controls the total memory envelope; the four fraction arguments determine how it is divided among the caches (they need not sum to 1 — each cache is sized independently as budget * frac).
Examples
julia> using Semisimple
julia> configure_caches!(budget = 512 * 1024^2) # 512 MiB totalSemisimple.cache_info — Function
cache_info() -> NamedTupleReturn a snapshot of the current cache occupancy. Each entry is a NamedTuple with fields length (number of entries) and maxsize (capacity in bytes).
Examples
julia> using Semisimple
julia> info = cache_info();
julia> info.tensor.length >= 0
trueInternals reference
These are internal functions not part of the public API. They are documented here for contributors and advanced users.
Root system internals
Semisimple._root_system_cache — Constant
RootSystem(::Type{DT}) -> RootSystem{DT,R}Return the root system for Dynkin type DT. A single instance is cached per Dynkin type; the data is computed by a compact value-level builder shared by all types.
Examples
julia> using Semisimple
julia> RootSystem(TypeA{2}) === RootSystem(TypeA{2})
trueSemisimple._make_root_system_runtime — Function
_make_root_system_runtime(::Type{DT}) -> RootSystem{DT,R}Compact runtime builder for root systems: value-level root enumeration (compiled once) plus a small per-rank conversion into SVector storage.
Weyl group internals
Semisimple._weyl_denominator — Function
_weyl_denominator(::Type{DT}) -> BigIntCompute the Weyl dimension denominator ∏_{α>0} ⟨ρ, α⟩.
Semisimple._weyl_dim_scaled_roots — Function
_weyl_dim_scaled_roots(::Type{DT}) -> Vector{SVector{R,Int}}Return the symmetrizer-scaled positive root vectors d .* α for Dynkin type DT.
Semisimple._explain_rmul — Function
Internal: determines what right-multiplication by s does to word x.
Returns (insert::Bool, position::Int, letter::UInt8):
- if
insert=true: insertletteratposition - if
insert=false: delete the element atposition
Semisimple.weylloop — Function
weylloop(action!, ::Type{DT}, v::AbstractVector{<:Integer})Call action!(w) once for each weight w in the Weyl orbit of v, where v and w are in the fundamental weight (ω) basis.
Uses LiE's ε-basis algorithm: converts to ε-coordinates where a classical subgroup acts by permutations (type A) or permutations + sign changes (types B/C/D), then enumerates orbits systematically via lexicographic permutation generation and Gray-code sign flips — with no hash set or BFS.
For exceptional types, coset representatives W / W_classical are precomputed as matrices.
The traversal is specialized on the rank and the action! closure, while the ε→ω transform is selected by a runtime family tag — so all Dynkin types of the same rank share one compiled traversal. The per-point output buffer is a stack-allocated MVector; the deduplicated suborbit representatives live in small heap vectors because their count depends on the Dynkin type and the input weight.
action! receives a mutable workspace vector; it must NOT hold a reference to this vector across calls (copy if needed).
Cache internals
Semisimple._apply_cache_preferences! — Function
_apply_cache_preferences!()Read Preferences-based cache settings and resize caches accordingly. Called from __init__() so that user preferences take effect on load.
Recognised preferences (set via Preferences.set_preferences!):
| Key | Type | Default |
|---|---|---|
cache_budget | Int | 25% of RAM (≥256 MiB) |
dominant_frac | Float64 | 0.30 |
tensor_frac | Float64 | 0.40 |
sym_power_frac | Float64 | 0.15 |
ext_power_frac | Float64 | 0.15 |
Character internals
Semisimple.dot_reduce — Function
dot_reduce(λ::WeightLatticeElem{DT,R}) -> Tuple{Int, WeightLatticeElem{DT,R}}Compute the "dot-action reduction" of λ:
Return (ε, μ) where:
μis the dominant weight such thatw ⋅ λ = μunder the dot actionw ⋅ λ = w(λ + ρ) - ρfor some Weyl group elementwε = (-1)^{ℓ(w)}is the sign ofw, orε = 0ifλ + ρis singular (lies on a Weyl chamber wall)
This is the key ingredient in the Brauer–Klimyk algorithm.
Semisimple.brauer_klimyk — Function
brauer_klimyk(char::Dict{SVector{R,Int}, <:Integer}, μ::WeightLatticeElem{DT,R}) -> WeylCharacter{DT,R}Tensor the representation with weight multiplicities char (as from freudenthal_formula) with the irreducible representation $\mathrm{V}(μ)$, using the Brauer–Klimyk formula:
$\mathrm{V} \otimes \mathrm{V}(μ) = \sum_{\text{weights } λ \text{ of } \mathrm{V}} m(λ) \cdot ε(λ+μ) \cdot \mathrm{V}(ν(λ+μ))$
where (ε, ν) = dot_reduce(μ + λ). Accepts BigInt-valued multiplicities (as returned by freudenthal_formula); the resulting irreducible multiplicities are stored as Int in the returned WeylCharacter, so this will throw InexactError if any of them exceeds typemax(Int64).
Semisimple._brauer_klimyk_dominant — Function
_brauer_klimyk_dominant(dom_char::Dict{SVector{R,Int}, <:Integer}, μ::WeightLatticeElem{DT,R}) -> WeylCharacter{DT,R}Like brauer_klimyk, but takes a dominant-only character dict (as returned by dominant_character) and expands Weyl orbits on-the-fly using weylloop. This avoids materializing the full weight system and eliminates hash-set overhead for large orbits.
The Brauer–Klimyk formula is: $\mathrm{V} \otimes \mathrm{V}(μ) = \sum_{\text{dom. wts } λ_d} m(λ_d) \sum_{w \in W \cdot λ_d} ε(w(λ_d)+μ) \cdot \mathrm{V}(ν(w(λ_d)+μ))$
Accepts BigInt-valued multiplicities; the resulting irreducible multiplicities are stored as Int in the returned WeylCharacter and will throw InexactError if any of them exceeds typemax(Int64).
Semisimple._expand_dominant_orbits — Function
_expand_dominant_orbits(::Type{DT}, dom_mults, k) -> Dict{SVector{R,Int}, BigInt}Expand a dominant-only multiplicity dictionary to the full weight system, scaling every weight by k (k = 1 for the plain character, k > 1 for the k-th Adams operator). Uses weylloop directly to avoid materialising intermediate orbit vectors. Shared by freudenthal_formula and adams_operator so that only one orbit-expansion closure is compiled per Dynkin type.
Semisimple._vdecomp — Function
_vdecomp(::Type{DT}, dom_char::Dict{SVector{R,Int},<:Integer}) -> WeylCharacter{DT,R}Virtual decomposition: given a character as a dict of weight multiplicities (dominant weights only, as produced by Adams operators), decompose into irreducibles using the Weyl orbit / alternating-dominant method.
This is the analogue of LiE's Vdecomp function.
Semisimple._tensor_characters — Function
_tensor_characters(V::WeylCharacter{DT,R}, W::WeylCharacter{DT,R}) -> WeylCharacter{DT,R}Tensor product of two virtual characters (each decomposed into irreducibles).
Littlewood–Richardson internals (type A)
Semisimple._weight_to_partition — Function
_weight_to_partition(λ::WeightLatticeElem{TypeA{N},N}) -> Vector{Int}Convert a dominant weight in fundamental weight coordinates to a partition with $N+1$ parts (for $\mathrm{GL}_{N+1}$).
For $\mathrm{A}_N$, the dominant weight $λ = (λ_1, …, λ_N)$ in the fundamental weight basis corresponds to the partition $μ = (μ_1 ≥ μ_2 ≥ ⋯ ≥ μ_N ≥ 0)$ where $μ_i = λ_i + λ_{i+1} + ⋯ + λ_N$ (partial sums from right to left), with $μ_{N+1} = 0$.
Semisimple._partition_to_weight — Function
_partition_to_weight(::Type{TypeA{N}}, p::Vector{Int}) -> WeightLatticeElem{TypeA{N},N}Convert a partition back to a dominant weight in the fundamental weight basis for $\mathrm{SL}_{N+1}$. First reduces the partition by subtracting the minimum part (to pass from $\mathrm{GL}$ to $\mathrm{SL}$), then computes successive differences: $λ_i = μ_i - μ_{i+1}$.
Semisimple._lr_coefficients — Function
_lr_coefficients(α::Vector{<:Integer}, β::Vector{<:Integer}, n::Integer) -> Dict{Vector{Int}, Int}Compute all Littlewood–Richardson coefficients $c^ν_{αβ}$ for partitions α and β, where partitions have at most n parts.
Returns a dictionary mapping each partition ν (as Vector{Int}) to the LR coefficient $c^ν_{αβ}$.
The algorithm fills the skew shape $ν / α$ with content β row by row, enforcing:
- Semistandard: entries weakly increase along rows, strictly increase down columns.
- Ballot (lattice word) condition: reading the filling right-to-left, top-to-bottom, at every prefix the count of
j≥ count ofj+1.
We enumerate valid fillings recursively row by row, which implicitly determines the partition ν.
Semisimple._n_tableaux — Function
_n_tableaux(λ::Vector{<:Integer}, l::Integer) -> BigIntCompute the number of standard Young tableaux of shape $λ$ using the hook-length formula:
$f^λ = \frac{n!}{\prod_{\text{boxes}} h(b)}$
l is the number of (nonzero) rows.
Plethysm internals (Murnaghan–Nakayama)
Semisimple._partitions — Function
_partitions(n::Integer) -> Vector{Vector{Int}}Generate all partitions of n in decreasing order.
Semisimple._classord — Function
_classord(κ::Vector{Int}) -> BigIntCompute the size of the conjugacy class in $S_n$ corresponding to cycle type $κ$:
$|\mathrm{Cl}(κ)| = \frac{n!}{\prod_{k>0} k^{c_k(κ)} \cdot c_k(κ)!}$
where $c_k(κ)$ is the number of parts of $κ$ equal to $k$. The parts of κ must be in weakly decreasing order.
Semisimple._mn_char_val — Function
_mn_char_val(λ::Vector{Int}, μ::Vector{Int}) -> BigIntCompute the irreducible $S_n$-character value $χ^λ(μ)$ using the Murnaghan–Nakayama rule.
Both λ and μ are partitions of $n$ with parts in weakly decreasing order.
The algorithm represents the Young diagram of $λ$ as a Maya diagram (edge sequence of horizontal/vertical steps), then recursively removes rim hooks of the sizes given by the parts of $μ$ (largest first).
Semisimple._mn_recurse! — Function
Recursive Murnaghan–Nakayama: remove rim hooks of sizes μ[i], μ[i+1], … from the Maya diagram edge, tracking leg-length parity in k.