Comrade.jl-Style Sky Models

Gay.jl includes a colored S-expression DSL inspired by Comrade.jl, the Event Horizon Telescope's VLBI imaging package.

Each sky model primitive gets a deterministic color from the splittable RNG, enabling reproducible visualizations of black hole and radio astronomy models.

The Connection

Comrade.jl uses Pigeons.jl for Bayesian inference. Pigeons uses SplittableRandoms.jl for reproducible MCMC chains. Gay.jl uses the same SplittableRandoms pattern for reproducible colors!

Comrade.jl (EHT imaging)
    └── Pigeons.jl (parallel tempering)
          └── SplittableRandoms.jl
                └── Gay.jl (deterministic colors)

Setup

using Gay

Sky Model Primitives

Gay.jl provides primitives matching VLBISkyModels.jl:

gay_seed!(2017)  # EHT M87* observation year
2017

Ring

A circular ring — the photon ring around a black hole

ring = comrade_ring(1.0, 0.3)  # radius=1.0, width=0.3
println("Ring: ", sky_show(ring))
Ring: (ring 1.0 0.3)

Gaussian

An elliptical Gaussian — central emission or jet base

gauss = comrade_gaussian(0.5, 0.3)  # σx=0.5, σy=0.3
println("Gaussian: ", sky_show(gauss))
Gaussian: (gaussian 0.5 0.3)

Disk

A uniform disk — filled circular region

disk = comrade_disk(0.4)  # radius=0.4
println("Disk: ", sky_show(disk))
Disk: (disk 0.4)

Crescent

An asymmetric ring — Doppler-boosted emission

crescent = comrade_crescent(1.2, 0.6, 0.3)  # r_out, r_in, shift
println("Crescent: ", sky_show(crescent))
Crescent: (crescent 1.2 0.6 0.3)

Composing Models

Combine primitives with sky_add — like Comrade's + operator:

gay_seed!(2017)

m87_model = sky_add(
    comrade_ring(1.0, 0.3),
    comrade_gaussian(0.5, 0.3)
)

println("\n=== M87* Style Model ===")
comrade_show(m87_model)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Ring(1.0, 0.3, RGB{Float64}(0.8987265781159566,0.9567537986610289,1.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Gaussian(0.5, 0.3, RGB{Float64}(0.5170790701795467,0.44792636874572234,1.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Different Black Hole Styles

Sgr A* Style (Galactic Center)

gay_seed!(2022)  # Sgr A* observation year

sgra_model = sky_add(
    comrade_crescent(1.2, 0.6, 0.3),
    comrade_disk(0.4)
)

println("\n=== Sgr A* Style Model ===")
comrade_show(sgra_model)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Crescent(1.2, 0.6, 0.3, RGB{Float64}(0.0,0.21066244035233272,0.5970438071964641)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Disk(0.4, RGB{Float64}(0.1284018629675431,0.5287270589843794,0.5637300452448932)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Multi-Ring Structure

gay_seed!(42069)

rings_model = sky_add(
    comrade_ring(0.6, 0.2),
    comrade_ring(0.9, 0.15),
    comrade_ring(1.2, 0.1),
    comrade_ring(1.5, 0.25)
)

println("\n=== Multi-Ring Model ===")
comrade_show(rings_model)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Ring(0.6, 0.2, RGB{Float64}(0.0,0.16256526175360417,0.396164822876032)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Ring(0.9, 0.15, RGB{Float64}(0.7638656216597285,0.0,0.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Ring(1.2, 0.1, RGB{Float64}(0.2706659457392915,1.0,0.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Ring(1.5, 0.25, RGB{Float64}(0.9973360765045345,0.8856020633603051,0.76562728863815)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Colored S-Expressions

Each primitive's parentheses are colored with its deterministic color. This makes complex model compositions visually parseable:

(ring 1.0 0.3) + (gaussian 0.5 0.3)
 ^^^^^          ^^^^^^^^^
 blue           green

The colors are determined by:

  1. The seed (gay_seed!)
  2. The order of primitive creation
  3. The splittable RNG state

Using the Model Builder

gay_seed!(1337)
model_m87 = comrade_model(seed=1337, style=:m87)
println("\n=== Built-in M87 Model ===")
comrade_show(model_m87)

gay_seed!(1337)
model_sgra = comrade_model(seed=1337, style=:sgra)
println("\n=== Built-in Sgr A* Model ===")
comrade_show(model_sgra)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Crescent(1.2, 0.6, 0.3, RGB{Float64}(1.0,0.5409670236205668,1.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Disk(0.4, RGB{Float64}(1.0,0.4755266585520956,0.8006909706115896)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Transformations

Apply Comrade-style modifiers:

gay_seed!(42)
base = sky_add(
    comrade_ring(1.0, 0.25),
    comrade_gaussian(0.3)
)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Ring(1.0, 0.25, RGB{Float64}(0.0,0.7327246286990455,0.0)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0))), (Gaussian(0.3, 0.3, RGB{Float64}(1.0,0.0,0.13508147896216222)), (flux = 1.0, stretch = (1.0, 1.0), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Stretch asymmetrically

stretched = sky_stretch(base, 1.5, 0.8)
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Ring(1.0, 0.25, RGB{Float64}(0.0,0.7327246286990455,0.0)), (flux = 1.0, stretch = (1.5, 0.8), rotate = 0.0, shift = (0.0, 0.0))), (Gaussian(0.3, 0.3, RGB{Float64}(1.0,0.0,0.13508147896216222)), (flux = 1.0, stretch = (1.5, 0.8), rotate = 0.0, shift = (0.0, 0.0)))], 1.0)

Rotate

rotated = sky_rotate(stretched, 0.3)  # radians
SkyModel(Tuple{SkyPrimitive, NamedTuple}[(Ring(1.0, 0.25, RGB{Float64}(0.0,0.7327246286990455,0.0)), (flux = 1.0, stretch = (1.5, 0.8), rotate = 0.3, shift = (0.0, 0.0))), (Gaussian(0.3, 0.3, RGB{Float64}(1.0,0.0,0.13508147896216222)), (flux = 1.0, stretch = (1.5, 0.8), rotate = 0.3, shift = (0.0, 0.0)))], 1.0)

Shift

shifted = sky_shift(rotated, 0.2, -0.1)

println("\n=== Transformed Model ===")
println("Original:    ", sky_show(base))
println("After transforms applied")

=== Transformed Model ===
Original:    (ring 1.0 0.25) + (gaussian 0.3 0.3)
After transforms applied

ASCII Intensity Maps

The comrade_show function renders colored ASCII intensity maps, showing the spatial structure of each model:

  • Ring → annular structure
  • Gaussian → central concentration
  • Crescent → asymmetric brightness
  • Disk → filled circle

Colors from each primitive blend additively.

Generate many models in parallel using SPI:

using Base.Threads

models = Vector{SkyModel}(undef, 1000)
@threads for i in 1:1000
    # Each thread gets independent RNG stream
    models[i] = comrade_model(seed=42069+i, style=rand([:m87, :sgra, :custom]))
end

All 1000 models are reproducible — same seeds give same models!

Connection to Real EHT Imaging

The primitives map to actual VLBISkyModels.jl types:

Gay.jlVLBISkyModels.jlPhysical Meaning
RingMRing convolvedPhoton ring
GaussianGaussianJet base, central emission
DiskDiskFilled emission region
CrescentCrescentDoppler-boosted asymmetry

The colors help distinguish components in complex fits.

println("\n◆ Comrade sky models example complete")

◆ Comrade sky models example complete