Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

IC redesign history

San Diego State University

In February 2026, progenax went through a comprehensive redesign of its initial-conditions API. The redesign replaced an inheritance-based, partially-stateful, implicitly-united design with the protocol-based, immutable, explicitly-united architecture documented elsewhere in this section. This chapter is the historical record of what was changed, why, and what the migration path looked like.

The pre-redesign API

Before the redesign, progenax’s IC API looked like this:

# Pre-redesign (deprecated, kept in legacy/)
from progenax_legacy import ClusterIC

ic = ClusterIC(
    N=1000,
    profile="plummer",      # String, not a class
    r_h=1.0,
    Q_vir=0.5,
)
ic.set_units("stellar")     # Mutable global state
ic.generate()                # Mutates ic.positions, ic.velocities
masses = ic.masses           # Read mutated state

Five things were wrong with this:

Problem

Concrete failure mode

String-keyed profile selection

profile="plummer" is a runtime check; typos surface only when generate() is called. No autocomplete; no type checking

Mutable IC state

ic.generate() mutates the object in place. Calling it twice silently re-randomises; users had to remember the call order

Global units

ic.set_units("stellar") set a module-level default. Tests that ran in different orders saw different units

Inheritance-based extension

Adding a new profile required subclassing BaseProfile. The base class had partial implementations that subclasses inherited; behaviour depended on which methods were overridden

Non-differentiable internals

The legacy code used Python loops, mutable arrays, and rejection sampling. Gradients did not flow through generate()

Each of these had a corresponding bug class. The “string-keyed” problem caused 30%\sim 30\% of all IC-related issues from new users. The “global units” problem caused the silent factor-8800 energy-mismatch bug documented at Units policy. The “inheritance” problem made adding new profiles a 3-day exercise.

The redesign principles

The redesign was driven by five principles, each addressing one of the pre-redesign problems:

Principle

Implementation

Type-safe composition

SpatialProfile / VelocityDF / IMFProtocol runtime-checkable protocols (Protocol-based composition)

Immutable PyTree state

Equinox modules; to_com_frame returns new arrays, never mutates (Three-brick state pattern)

Explicit units

No global state; every core API takes explicit G or units (Units policy)

Composition over inheritance

Each profile is its own self-contained module; no base class

Differentiable end-to-end

Every step uses lax.scan / vmap; jax.grad flows through builders (Differentiability rules)

The principles compose: protocol-based composition naturally produces immutable PyTrees because each implementation is a self-contained Equinox module; explicit units naturally cohere with the no-global-state policy; differentiability follows from the mutation-free, fixed-iteration discipline.

The migration

The redesign was breaking. Old user code did not work against the new API, and there was no compatibility shim — the legacy package preserved the old code unchanged but the new progenax did not import it. Users had two paths:

Path

When to use

Migrate to the new API

All new work, all production code. Most pre-redesign user code rewrites in 30\sim 30 minutes given the new API’s verbosity

Reproduce pre-redesign results

Check out the pre-redesign commit from git history (the redesign is documented in the development log). There is no separate legacy package or install.

The migration guide (originally at docs/migration_2026_02_12.md, absorbed into IC redesign spec (2026-02-12)) walked through the per-call rewrites. A typical migration:

# Before
ic = ClusterIC(N=1000, profile="plummer", r_h=1.0, Q_vir=0.5)
ic.set_units("stellar")
ic.generate()
masses, positions, velocities = ic.masses, ic.positions, ic.velocities

# After
from progenax.profiles import PlummerProfile
from progenax.kinematics import PlummerVelocityDF
from progenax.imf import Maschberger
from progenax.builders import virial_scale
from jaxstro.units import STELLAR

masses = Maschberger(alpha=2.3).sample(key, 1000)
profile = PlummerProfile(r_h=1.0)
df = PlummerVelocityDF(r_h=1.0)
positions = profile.sample_positions(masses, key)
velocities = df.sample_velocities(positions, masses, key, G=STELLAR.G)
velocities = virial_scale(
    positions, velocities, masses, Q_target=0.5, G=STELLAR.G
)

Lines went from 5\sim 5 to 10\sim 10. In exchange, the new code is JIT-compatible, vmap-compatible, differentiable, type-checked, and runs 100×\sim 100\times faster on GPU.

What was kept

Despite the breaking nature, two things from the pre-redesign API were preserved:

  1. The half-mass-radius parameterisation. Every profile is still parameterised by rhr_h, not by its internal scale radius. This was the right convention even in the legacy API and continued.

  2. The Qvir=T/VQ_{\mathrm{vir}} = T/|V| convention. The factor-of-2 alternatives were rejected pre-redesign and stayed rejected; see Virial Q convention (Q = T/|V|).

These conventions are cultural — they could have been changed in the redesign but were intentionally preserved to maintain cross-version comparability of cluster sizes and dynamical states.

What was added in the redesign

Beyond the five core changes, the redesign added several capabilities that were technically possible before but not practical:

The redesign was therefore not just a refactor: it unlocked science that the legacy API could not have supported.

Lessons learned

Three lessons from the redesign that inform later work:

  1. Breaking APIs are sometimes the right answer. The compatibility-shim alternative would have produced a hybrid API that looked like neither the old nor the new, with the worst features of both. The clean break — old code in legacy, new code in progenax — was simpler and clearer.

  2. The protocol-based pattern was a discovery, not an invention. The original design proposal used inheritance; protocols were adopted halfway through after equinox-module compatibility problems made inheritance untenable. The pattern is now central to progenax’s identity.

  3. Migration is the bottleneck. The actual technical migration was 1 month of work. The social migration — getting users to rewrite their notebooks, updating tutorials, fielding support questions — took 6 months. Future redesigns will allocate the same ratio.

References

The redesign spec is in the development log at IC redesign spec (2026-02-12). The current architecture it produced is documented in this chapter family: JAX-native philosophy, Three-brick state pattern, Protocol-based composition, Differentiability rules, Units policy, Virial Q convention (Q = T/|V|). The PP20 ζ(p) transcription bug fix at PP20 ζ(p) transcription fix was a post-redesign bug — orthogonal to the architecture work — but is part of the same “clean physics, anchored on tests” approach the redesign codified.

References
  1. Goodwin, S. P., & Whitworth, A. P. (2004). The dynamical evolution of fractal star clusters: The survival of substructure. Astronomy and Astrophysics, 413, 929–937. 10.1051/0004-6361:20031529