Version: 1.0 Status: RFC (Request for Comments) Last Updated: 2025-11-21
This specification extends Morphogen's Transform Dialect with composable named transforms — reusable, chainable transformation pipelines that can be defined once and used throughout a program.
Key Ideas:
- Named transforms as first-class language constructs
- Automatic inversion for invertible transforms
- Composition algebra for building complex pipelines
- Type-safe transform chaining with representation tracking
- Domain-polymorphic transforms that work across domains
Prerequisites:
- Transform Dialect — Base transform operations
- Universal DSL Principles — Design philosophy
- Continuous-Discrete Semantics — Execution models
Related:
- Operator Foundations — Spectral and operator theory
- Categorical Structure — Functorial semantics
Currently:
// Repetitive transform chains
let spec1 = signal1 |> fft |> magnitude |> mel_scale |> log
let spec2 = signal2 |> fft |> magnitude |> mel_scale |> log
let spec3 = signal3 |> fft |> magnitude |> mel_scale |> log
Issues:
- Code duplication
- No automatic inverse
- No reusability across programs
- No composition
Define once:
@transform audio_to_mel_spectrogram {
signal : Stream<f32, audio:time>
-> fft
-> magnitude
-> mel_scale(n_mels=128)
-> log(offset=1e-6)
-> Stream<f32, audio:mel>
}
Use everywhere:
let mel_spec1 = audio_to_mel_spectrogram(signal1)
let mel_spec2 = audio_to_mel_spectrogram(signal2)
// Automatic inverse (when possible)
let reconstructed = inv(audio_to_mel_spectrogram)(mel_spec1)
Benefits:
- ✅ Reusable across program
- ✅ Automatic type inference
- ✅ Composable with other transforms
- ✅ Self-documenting
- ✅ Invertible (when theoretically possible)
Syntax:
@transform <name> {
<input_type>
-> <transform1>
-> <transform2>
-> ...
-> <output_type>
}
Example:
@transform time_to_frequency {
Stream<f32, audio:time, 48kHz>
-> fft(window="hann", norm="ortho")
-> Stream<Complex<f32>, audio:frequency, 24kHz>
}
Syntax:
@transform <name>(params...) {
...
}
Example:
@transform mel_spectrogram(n_fft=2048, hop_length=512, n_mels=128) {
Stream<f32, audio:time>
-> stft(n_fft, hop_length, window="hann")
-> magnitude
-> mel_scale(n_mels)
-> log(offset=1e-6)
-> Stream<f32, audio:mel>
}
Usage:
// Use default parameters
let mel1 = mel_spectrogram(signal)
// Override parameters
let mel2 = mel_spectrogram(signal, n_mels=256, n_fft=4096)
Compose transforms with ∘ (compose operator):
@transform audio_features = mel_spectrogram ∘ normalize ∘ delta_features
// Equivalent to:
let features = signal
|> mel_spectrogram
|> normalize
|> delta_features
Composition is associative:
(f ∘ g) ∘ h = f ∘ (g ∘ h)
Automatic inverse (when mathematically possible):
@transform fwd {
x -> fft -> magnitude -> y
}
// Inverse is automatically defined (if invertible)
let y = fwd(x)
let x_reconstructed = inv(fwd)(y) // May be approximate
Invertibility properties:
| Transform | Invertible? | Notes |
|---|---|---|
fft |
✅ Yes | Exact (unitary) |
magnitude |
❌ No | Loses phase information |
mel_scale |
Non-linear warping, can pseudo-invert | |
log |
✅ Yes | exp is exact inverse |
normalize |
If normalization stats are stored |
Compiler behavior:
- Exact inverse: Compiler generates exact inverse
- Approximate inverse: Compiler warns, generates best-effort inverse
- No inverse: Compile error if
inv()is called
Input and output types are tracked:
@transform mel_spectrogram {
Stream<f32, audio:time, R>
-> stft -> magnitude -> mel_scale -> log
-> Stream<f32, audio:mel, R/hop_length>
}
// Type checker verifies:
input : Stream<f32, audio:time, 48kHz>
output : Stream<f32, audio:mel, 93Hz> // 48000 / 512
Type error example:
@transform invalid {
Stream<f32, audio:time>
-> fft // OK: time -> frequency
-> diffuse(rate=0.1, dt=0.01) // ERROR: diffuse expects Field2D, got Spectrum
}
Domains define valid representations:
domain audio {
representations: [time, frequency, cepstral, mel]
// Valid transform paths
time -> frequency: fft, stft
frequency -> time: ifft, istft
frequency -> mel: mel_scale
time -> cepstral: dct
}
Compiler enforces valid paths:
// OK: time -> frequency -> mel
audio.time |> fft |> mel_scale
// ERROR: No direct path time -> mel (must go through frequency)
audio.time |> mel_scale // Compile error
Identity transform:
@transform identity {
x -> x
}
// Laws:
identity ∘ f = f
f ∘ identity = f
Associativity:
(f ∘ g) ∘ h = f ∘ (g ∘ h)
Inverse laws (when invertible):
inv(f) ∘ f = identity
f ∘ inv(f) = identity
Composition inverse:
inv(f ∘ g) = inv(g) ∘ inv(f) // Reverse order
Define feature extraction pipeline:
@transform audio_to_mfcc(n_fft=2048, n_mels=128, n_mfcc=13) {
Stream<f32, audio:time>
-> stft(n_fft, hop_length=512)
-> magnitude
-> mel_scale(n_mels)
-> log(offset=1e-6)
-> dct(type=2, norm="ortho")
-> take_first(n_mfcc) // Keep first N coefficients
-> Stream<f32, audio:mfcc, 93Hz>
}
// Use in program
use audio
@state recording : AudioBuffer = audio.load("speech.wav")
flow() {
let mfcc = audio_to_mfcc(recording)
output mfcc
}
Define spectral solver for Poisson equation:
@transform spectral_poisson_solve(laplacian_eigenvalues) {
Field2D<f32>
-> fft2d // Spatial -> k-space
-> divide_elementwise(laplacian_eigenvalues) // Solve in k-space
-> ifft2d // k-space -> spatial
-> Field2D<f32>
}
// Use for fast Poisson solve
use field
@state rhs : Field2D<f32> = initialize_source()
@state solution : Field2D<f32>
flow() {
// Solve ∇²φ = rhs in Fourier space (O(N log N) instead of O(N²))
solution = spectral_poisson_solve(rhs, laplacian_eigenvalues=compute_eigenvalues())
}
Define Hamiltonian phase space transform:
@transform canonical_coordinates_to_hamiltonian {
State<position: Vec3, momentum: Vec3>
-> compute_kinetic_energy
-> compute_potential_energy
-> sum_energies
-> Hamiltonian<f32>
}
// Enables analysis in energy space
use physics
@state particles : State<position, momentum>
flow(dt=0.01) {
let H = canonical_coordinates_to_hamiltonian(particles)
// Energy should be conserved (check)
assert(abs(H - H_initial) < 1e-6)
}
Chain transforms across domains:
// Audio -> Visual pipeline
@transform audio_to_visual {
Stream<f32, audio:time>
-> mel_spectrogram(n_mels=64) // Audio domain
-> normalize(mean=0.5, std=0.2)
-> to_image(colormap="viridis") // Visual domain
-> Stream<RGB, visual, 30Hz>
}
// Use for real-time visualization
use audio, visual
@state mic_input : Stream<f32, audio:time> = audio.record()
flow() {
let viz = audio_to_visual(mic_input)
visual.display(viz)
}
Transforms are registered like operators:
# morphogen/stdlib/transforms/audio.py
@composable_transform(
name="mel_spectrogram",
domain="audio",
input_repr="time",
output_repr="mel",
invertible="approximate"
)
def mel_spectrogram(
signal,
n_fft=2048,
hop_length=512,
n_mels=128
):
"""Convert audio signal to mel-scaled spectrogram."""
# Pipeline: stft -> magnitude -> mel_scale -> log
spec = stft(signal, n_fft=n_fft, hop_length=hop_length)
mag = magnitude(spec)
mel = mel_scale(mag, n_mels=n_mels)
return log(mel + 1e-6)Automatic inverse for invertible transforms:
# Compiler generates inverse
def inv_mel_spectrogram(mel_spec, n_fft=2048, hop_length=512, n_mels=128):
"""Approximate inverse of mel_spectrogram."""
# Pipeline: exp -> inv_mel_scale -> istft
mag = exp(mel_spec)
spec_mag = inv_mel_scale(mag, n_mels=n_mels)
# Phase reconstruction (approximate - use Griffin-Lim)
spec = phase_reconstruction(spec_mag, method="griffin_lim")
return istft(spec, hop_length=hop_length)Compiler fuses composed transforms:
@transform pipeline = f ∘ g ∘ h
// Compiled as single fused kernel (when possible)
let result = pipeline(input)
// Instead of:
// temp1 = h(input)
// temp2 = g(temp1)
// result = f(temp2)
Fusion rules:
- Consecutive spectral transforms → single FFT
- Consecutive element-wise ops → single kernel
- Consecutive filters → frequency-domain multiplication
Choose transform based on runtime condition:
@transform adaptive_denoise(noise_level) {
if noise_level > 0.5:
signal -> wavelet_denoise(threshold=0.3)
else:
signal -> gaussian_blur(sigma=1.0)
}
Transforms with multiple inputs:
@transform cross_correlation {
(signal1: Stream<f32>, signal2: Stream<f32>)
-> (fft(signal1), fft(signal2))
-> multiply_conjugate
-> ifft
-> Stream<f32, correlation>
}
Transforms with learnable parameters:
@transform learned_encoder(params: NeuralNetParams) {
Image<RGB>
-> apply_neural_net(params)
-> Embedding<f32, 512>
}
// Parameters updated during training
flow() {
let embedding = learned_encoder(image, params=trained_params)
}
| Transform | Input | Output | Invertible? |
|---|---|---|---|
fft |
time | frequency | ✅ Exact |
stft |
time | time-frequency | ✅ Exact |
mel_spectrogram |
time | mel | |
mfcc |
time | mfcc | ❌ No (lossy) |
chromagram |
time | chroma |
| Transform | Input | Output | Invertible? |
|---|---|---|---|
fft2d |
spatial | k-space | ✅ Exact |
wavelet2d |
spatial | wavelet | ✅ Exact |
dct2d |
spatial | dct | ✅ Exact |
eigenbasis |
standard | eigen | ✅ Exact |
| Transform | Input | Output | Invertible? |
|---|---|---|---|
position_to_phase |
position | phase-space | ✅ Exact |
energy_to_action |
energy | action | ✅ Exact |
cart_to_polar |
cartesian | polar | ✅ Exact |
Basic transforms:
fft,ifft(1D)stft,istft(2D time-frequency)dct(cepstral)fft2d,ifft2d(spatial)
Operators exist, but not as composable named transforms.
Language features:
@transformdeclaration syntax- Transform composition (
∘) - Automatic inverse (
inv()) - Parametric transforms
- Type-safe composition
Transform catalog:
- Audio:
mel_spectrogram,mfcc,chromagram - Field: Spectral Poisson solver, wavelet denoise
- Physics: Phase space, Hamiltonian, canonical
Advanced features:
- Conditional transforms (runtime dispatch)
- Multi-input/multi-output transforms
- Learned transforms (neural networks)
- Adaptive transforms (parameter tuning)
- Transform equivalence (automatic simplification)
Define a named transform if:
- ✅ Pipeline used multiple times in program
- ✅ Common pattern in domain (e.g., MFCC in audio)
- ✅ Reusable across programs
- ✅ Self-documenting (name explains what it does)
Don't define if:
- ❌ Used only once
- ❌ Highly specific to one use case
- ❌ Too simple (single operation)
Mark as invertible if:
- ✅ Mathematically invertible (FFT, rotation, etc.)
- ✅ Lossless (no information dropped)
Mark as approximate if:
⚠️ Information lost but reconstruction possible (mel scale)⚠️ Phase lost but magnitude preserved⚠️ Requires additional assumptions (Griffin-Lim)
Mark as non-invertible if:
- ❌ Fundamentally lossy (dimensionality reduction without inverse)
- ❌ No known inverse method
Specifications:
- Transform Dialect — Base transform operations
- Type System — Type inference and checking
Philosophy:
- Universal DSL Principles — Design foundations
- Operator Foundations — Spectral theory
Architecture:
- Continuous-Discrete Semantics — Execution models
- Domain Architecture — Domain specifications
ADRs:
- Universal Domain Translation — Translation framework
Composable named transforms enable:
- Reusability — Define once, use everywhere
- Composition — Chain transforms algebraically (
f ∘ g ∘ h) - Invertibility — Automatic inverse when theoretically possible
- Type safety — Representation tracking and validation
- Self-documentation — Names capture intent
Example:
@transform audio_to_mel_spectrogram {
signal -> stft -> magnitude -> mel_scale -> log
}
// Use anywhere
let mel = audio_to_mel_spectrogram(recording)
// Automatic approximate inverse
let reconstructed = inv(audio_to_mel_spectrogram)(mel)
This makes transform-first thinking practical and powerful.
Next: See Universal Domain Translation for cross-domain translation semantics, or Transform Dialect for base transform operations.