Skip to content

Commit 6564732

Browse files
sirus20x6claude
andcommitted
feat(accrete): parametric giant migration model behind -r (Phase 1)
Implements migrate_giant() per research/modern/11-giant-migration.md, replacing the crude uniform-random relocation in dist_planetary_masses (accrete.cpp). Behind allow_planet_migration (-r), default OFF -> the default golden path is byte- identical (verify.sh --determinism ref=294147B, unchanged from pre-migration; goldens clean; ctest 23/23). Model: inward drift a_final = a_form*exp(-dt/tau) to an inner trap (~0.05 AU disk edge), tau = viscous Type II (a^1.5) above the ~0.5 M_Jup gap mass (Crida 2006) or Tanaka 2002 Type I (M^-1 a^(25/14)) below, with the empirical rate reduction in MIGRATION_EFFICIENCY; hot orbits (perihelion < ~0.05 AU) tidally circularized; only bodies > 0.1 M_Jup migrate (terrestrials untouched -> inner architecture preserved). Determinism: a FIXED two-draw order from the per-system RandomContext and an ANALYTIC perihelion clamp (no rejection loops). Verified -r byte-identical across -T1/-T2/-T4/-T8. Calibration (MIGRATION_EFFICIENCY=1e-4, 600-seed harness --migrate): hot-Jupiter occurrence (a<0.1 AU) 0.5% (matches transit rate Howard12/Fressin13) giant a-bins: <0.1:3 0.1-1:35 1-3:39 3-10:504 >10:325 (cold peak preserved) mass p50/p90 0.71 / 343 M_earth (no inflation) density-sane 0.946 architecture (-r): peas 0.28, hill 19, sigma_e 0.052, min period-ratio 1.012 (>1) The 1e-4 efficiency is a calibrated value (not derived): StarGen's snapshot Type I/II model + the steep isothermal Tanaka rate over-migrate massively, so a strong suppression is needed to match the observed ~0.5% HJ rate -- consistent with the known Type-I over-efficiency problem (Ida & Lin 2008a reduce by >=10x; the snapshot model needs more). Also adds --migrate to validate_population.py (passes -r) to measure it. KNOWN LIMITATIONS (documented follow-ups, NOT migration bugs): - cold:hot ratio is ~169 (target ~10-20) ONLY because StarGen over-produces cold giants (~84% of systems vs observed ~10-20%) -- a giant-FREQUENCY issue distinct from migration (giant occurrence / metallicity). - giant eccentricity dichotomy not achieved (<e>~0 hot AND cold): the post-accretion Cresswell-Nelson gas damping circularizes migration's broad e. Reconciling needs exempting cold giants from the damping, which changes the default path/goldens -- deferred to its own PR. - hot Jupiters are mostly 0.3-0.5 M_Jup (Type I cores); the snapshot model does not carry a >1 M_Jup core inward (it is Type II by final mass) -- a model limitation. Stays opt-in (-r) per the plan's phased rollout; default-ON is a later, separately- gated decision (needs the cold-frequency + eccentricity follow-ups first). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ff9e03 commit 6564732

3 files changed

Lines changed: 107 additions & 42 deletions

File tree

accrete.cpp

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,76 @@ void accrete::coalesce_planetesimals(long double a, long double e, long double m
747747
* @return planet*
748748
*/
749749

750+
// --- Giant migration (research/modern/11-giant-migration.md; -r only) ----------
751+
// Parametric inward disk migration of a giant / giant-core. Deterministic: a FIXED
752+
// number of draws (two) in a FIXED order from the per-system RandomContext, and an
753+
// ANALYTIC perihelion clamp (no rejection loops), so serial == parallel and the
754+
// stream is robust to threshold tweaks. Modifies a and e in place. Runs only under
755+
// allow_planet_migration (-r), so the default golden path never executes it.
756+
static void migrate_giant(long double &a, long double &e,
757+
long double total_mass_solar, long double stell_mass_ratio,
758+
long double min_distance, RandomContext *rng) {
759+
const long double a_form = a;
760+
const long double m_p_earth = total_mass_solar * SUN_MASS_IN_EARTH_MASSES;
761+
762+
// Fixed draw order (always two): available-time fraction, then ecc quantile.
763+
const long double u_t = rng->randDouble(0.0L, 1.0L);
764+
const long double u_e = rng->randDouble(0.0L, 1.0L);
765+
766+
// Inner trap: disk inner edge / magnetospheric cavity, never inside min_distance.
767+
long double a_stop = MIGRATION_INNER_EDGE_AU;
768+
if (a_stop < min_distance) {
769+
a_stop = min_distance;
770+
}
771+
772+
// Migration timescale (yr): viscous Type II above the gap-opening mass, faster
773+
// Tanaka Type I below it; efficiency folds in the empirical rate reduction.
774+
const long double gap_mass_earth =
775+
MIGRATION_GAP_MASS_MJUP * JUPITER_MASS * stell_mass_ratio;
776+
long double tau_yr;
777+
if (m_p_earth >= gap_mass_earth) {
778+
tau_yr = MIGRATION_TYPE2_NORM_YR * std::pow(a_form, 1.5L) / sqrt(stell_mass_ratio);
779+
} else {
780+
tau_yr = MIGRATION_TYPE1_NORM_YR / m_p_earth * std::pow(a_form / 10.0L, 25.0L / 14.0L);
781+
}
782+
tau_yr /= MIGRATION_EFFICIENCY;
783+
if (tau_yr <= 0.0L) {
784+
return;
785+
}
786+
787+
// Available migration time = fraction of disk lifetime (early formers get the
788+
// full budget). The spread carves the hot/warm/cold distribution.
789+
const long double dt = u_t * DISK_LIFETIME_YEARS;
790+
long double a_new = a_form * std::exp(-dt / tau_yr);
791+
if (a_new < a_stop) {
792+
a_new = a_stop;
793+
}
794+
if (a_new > a_form) {
795+
a_new = a_form; // inward only
796+
}
797+
798+
// Eccentricity: broad Rayleigh for cold/warm giants; tidal circularization for
799+
// hot orbits whose perihelion crosses the circularization locus.
800+
long double e_broad = GIANT_ECC_SIGMA * sqrt(-2.0L * std::log(1.0L - u_e));
801+
if (e_broad > GIANT_ECC_MAX) {
802+
e_broad = GIANT_ECC_MAX;
803+
}
804+
long double e_new = e_broad;
805+
if (a_new * (1.0L - e_broad) < MIGRATION_CIRC_AU) {
806+
e_new = 0.0L; // tides circularize hot Jupiters
807+
}
808+
// Analytic clamp: keep perihelion >= min_distance without a rejection loop.
809+
if (a_new > 0.0L && a_new * (1.0L - e_new) < min_distance) {
810+
e_new = 1.0L - (min_distance / a_new);
811+
if (e_new < 0.0L) {
812+
e_new = 0.0L;
813+
}
814+
}
815+
816+
a = a_new;
817+
e = e_new;
818+
}
819+
750820
auto accrete::dist_planetary_masses(sun &the_sun, long double inner_dust,
751821
long double outer_dust,
752822
long double outer_planet_limit,
@@ -902,45 +972,15 @@ auto accrete::dist_planetary_masses(sun &the_sun, long double inner_dust,
902972
else {
903973
min_distance = 0.015;
904974
}
905-
long double new_a = 0;
906-
long double new_e = 0;
907-
new_a = random_number(min_distance, a);
908-
new_e = random_eccentricity(ecc_coef);
909-
for (int i = 0;(calcPerihelion(new_a, new_e) < min_distance) && (i < 1000); i++) {
910-
new_a = random_number(min_distance, a);
911-
new_e = random_eccentricity(ecc_coef);
912-
}
913-
if (total_mass >= ((0.7 * JUPITER_MASS) / SUN_MASS_IN_EARTH_MASSES)) {
914-
if (random_ctx->randInt(14) == 0 && min_distance < 0.2) {
915-
new_a = random_number(min_distance, 0.2);
916-
new_e = random_eccentricity(ecc_coef);
917-
for (int i = 0;(calcPerihelion(new_a, new_e) < min_distance) && (i < 1000); i++) {
918-
new_a = random_number(min_distance, 0.2);
919-
new_e = random_eccentricity(ecc_coef);
920-
}
921-
}
922-
if (random_ctx->randInt(10) == 0 &&
923-
calcPerihelion(new_a, 0.1) > min_distance) {
924-
while (new_e < 0.1) {
925-
new_e = random_eccentricity(0.25);
926-
for (int i = 0;(calcPerihelion(new_a, new_e) < min_distance) && (i < 1000); i++) {
927-
new_a = random_number(min_distance, a);
928-
new_e = random_eccentricity(ecc_coef);
929-
}
930-
}
931-
}
932-
} else if (total_mass >= (2.0 / SUN_MASS_IN_EARTH_MASSES) &&
933-
total_mass <= (10.0 / SUN_MASS_IN_EARTH_MASSES) &&
934-
a >= (4.0 * sqrt(stell_luminosity_ratio)) &&
935-
(dust_mass / total_mass) >= 0.75) {
936-
if (random_ctx->randInt(4) == 0) {
937-
new_a = random_number(min_distance, a / 2.0);
938-
new_e = random_eccentricity(ecc_coef);
939-
for (int i = 0;calcPerihelion(new_a, new_e) < planet_inner_bound && i < 1000;i++) {
940-
new_a = random_number(min_distance, a / 2.0);
941-
new_e = random_eccentricity(ecc_coef);
942-
}
943-
}
975+
long double new_a = a;
976+
long double new_e = e;
977+
// Only giants / giant-cores migrate; terrestrials stay at their
978+
// formation orbit (protects the inner peas-in-a-pod architecture).
979+
// See research/modern/11-giant-migration.md and migrate_giant() above.
980+
if ((total_mass * SUN_MASS_IN_EARTH_MASSES) >=
981+
(MIGRATION_MIN_MASS_MJUP * JUPITER_MASS)) {
982+
migrate_giant(new_a, new_e, total_mass, stell_mass_ratio, min_distance,
983+
random_ctx);
944984
}
945985
a = new_a;
946986
e = new_e;

const.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,27 @@ constexpr double DISK_LIFETIME_YEARS = 3.0E6;
186186
constexpr double DISK_SIGMA0_SOLAR_PER_AU2 = 1.0E-6;
187187
constexpr double DISK_DAMP_BRACKET_FLOOR = 0.10;
188188

189+
/* Giant migration (research/modern/11-giant-migration.md; behind -r, default off).
190+
* A parametric inward drift a_final = a_form*exp(-dt/tau) to an inner trap, with
191+
* tidal circularization of hot orbits. tau follows Tanaka 2002 Type I
192+
* (tau ~ M_p^-1 a^(25/14)) below the gap-opening mass and a viscous Type II
193+
* (tau ~ a^1.5) above it (Crida 2006 q_c~5e-4 -> ~0.5 M_Jup); the empirical
194+
* efficiency reduction (Ida & Lin 2008a) is folded into MIGRATION_EFFICIENCY. The
195+
* inner trap is the disk inner edge / magnetospheric cavity (~0.05 AU; Lin,
196+
* Bodenheimer & Richardson 1996) and the tidal-circularization locus is ~0.04-0.05
197+
* AU (Wang 2023). Cold/warm giants keep a broad Rayleigh eccentricity
198+
* (<e>~0.27; Kipping 2013). These are CALIBRATION knobs tuned against the
199+
* Phase-0 population baseline (scripts/validate_population.py --migrate). */
200+
constexpr double MIGRATION_MIN_MASS_MJUP = 0.1; // only bodies > 0.1 M_Jup migrate
201+
constexpr double MIGRATION_GAP_MASS_MJUP = 0.5; // Type II gap-opening threshold (per M_star)
202+
constexpr double MIGRATION_TYPE1_NORM_YR = 1.1E5; // Tanaka 2002 Type I normalization (yr)
203+
constexpr double MIGRATION_TYPE2_NORM_YR = 7.0E5; // Type II viscous normalization at 1 AU (yr)
204+
constexpr double MIGRATION_EFFICIENCY = 0.0001; // overall rate multiplier (primary calibration knob)
205+
constexpr double MIGRATION_INNER_EDGE_AU = 0.05; // inner trap a_stop (disk edge / cavity)
206+
constexpr double MIGRATION_CIRC_AU = 0.05; // tidal circularization locus (hot -> e=0)
207+
constexpr double GIANT_ECC_SIGMA = 0.215; // Rayleigh sigma -> <e>~0.27 for cold giants
208+
constexpr double GIANT_ECC_MAX = 0.80; // eccentricity cap
209+
189210
// SI-unit physical constants used by the enviro.cpp acceleration helpers. These
190211
// are the EXACT values those functions previously redefined locally, kept
191212
// verbatim so generated output stays byte-identical. They intentionally differ

scripts/validate_population.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def repo_root() -> str:
4343
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4444

4545

46-
def generate(bin_path: str, work: str, seed: int, mass: float) -> dict | None:
46+
def generate(bin_path: str, work: str, seed: int, mass: float, migrate: bool = False) -> dict | None:
4747
"""Run stargen for one seed and return the parsed system, or None on failure."""
4848
html = os.path.join(work, "html")
4949
os.makedirs(html, exist_ok=True)
@@ -56,8 +56,11 @@ def generate(bin_path: str, work: str, seed: int, mass: float) -> dict | None:
5656
os.remove(out)
5757
except FileNotFoundError:
5858
pass
59+
argv = [bin_path, f"-s{seed}", f"-m{mass}", "-JS", "-o", "sys"]
60+
if migrate:
61+
argv.append("-r") # enable giant migration (research/modern/11-giant-migration.md)
5962
rc = subprocess.run(
60-
[bin_path, f"-s{seed}", f"-m{mass}", "-JS", "-o", "sys"],
63+
argv,
6164
cwd=work, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
6265
)
6366
if rc.returncode != 0 or not os.path.exists(out):
@@ -311,6 +314,7 @@ def main() -> int:
311314
ap.add_argument("--mass", type=float, default=1.0)
312315
ap.add_argument("--json", action="store_true", help="emit metrics as JSON")
313316
ap.add_argument("--check", action="store_true", help="exit nonzero on gross-sanity violation")
317+
ap.add_argument("--migrate", action="store_true", help="enable giant migration (passes -r)")
314318
args = ap.parse_args()
315319

316320
if not os.path.exists(args.bin):
@@ -323,7 +327,7 @@ def main() -> int:
323327
systems = []
324328
with tempfile.TemporaryDirectory(dir=os.environ["TMPDIR"]) as work:
325329
for i in range(args.seeds):
326-
s = generate(args.bin, work, args.seed_start + i, args.mass)
330+
s = generate(args.bin, work, args.seed_start + i, args.mass, args.migrate)
327331
if s is not None:
328332
systems.append(s)
329333

0 commit comments

Comments
 (0)