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 GaySky Model Primitives
Gay.jl provides primitives matching VLBISkyModels.jl:
gay_seed!(2017) # EHT M87* observation year2017Ring
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 greenThe colors are determined by:
- The seed (
gay_seed!) - The order of primitive creation
- 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) # radiansSkyModel(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 appliedASCII 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.
Gallery Generation
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]))
endAll 1000 models are reproducible — same seeds give same models!
Connection to Real EHT Imaging
The primitives map to actual VLBISkyModels.jl types:
| Gay.jl | VLBISkyModels.jl | Physical Meaning |
|---|---|---|
Ring | MRing convolved | Photon ring |
Gaussian | Gaussian | Jet base, central emission |
Disk | Disk | Filled emission region |
Crescent | Crescent | Doppler-boosted asymmetry |
The colors help distinguish components in complex fits.
println("\n◆ Comrade sky models example complete")
◆ Comrade sky models example complete