"""Stage `seabed`: sea-floor data for every ocean cell — vent potential, sea-floor type, minerals, bottom temperature, sediment. Land cells get 0 / "—" (the fields describe the sea floor).""" from __future__ import annotations import numpy as np from scipy.spatial import cKDTree from . import plateaus as PL from .config import params from .elevation import hotspot_track from .graph import distance_to from .noise import fbm from .sphere import latlon_to_xyz (SB_NONE, SB_TERRIGENOUS, SB_CARBONATE, SB_CLAY, SB_BASALT, SB_VOLCANIC, SB_CONTINENTAL, SB_VENTS, SB_TRENCH) = range(9) SEABED_NAMES = ["—", "terrigenous sediment", "carbonate ooze", "pelagic clay", "young basalt", "volcanic", "continental / plateau", "vent field", "trench"] MI_NONE, MI_SULFIDES, MI_NODULES, MI_COBALT, MI_PHOSPHORITE = range(5) MINERAL_NAMES = ["none", "polymetallic sulfides", "manganese nodules", "cobalt crusts", "phosphorite"] DEFAULTS = {"ridge_km": 100.0, "back_arc_km": 300.0, "back_arc_width_km": 150.0, "back_arc": 0.6, "hotspot_km": 150.0, "hotspot_spacing_km": 150.0, "terrigenous_km": 300.0, "carbonate_depth_m": 4500.0, "carbonate_t_c": 10.0, "young_myr": 10.0, "trench_km": 80.0, "trench_depth_m": 5000.0, "vent_field": 0.8, "sulfides": 0.7, "volcanic": 0.3, "thermocline_m": 800.0} def _cells_near(tree, radius_km, p, reach_km): return np.asarray(tree.query_ball_point(p, 2.0 * np.sin(min(np.pi, reach_km / radius_km) / 2.0)), dtype=np.int64) def _bump(v, g, tree, p, r_km, amp): i = _cells_near(tree, g.radius_km, p, 3.0 * r_km) if len(i): d = g.radius_km * np.arccos(np.clip(g.xyz[i] @ p, -1.0, 1.0)) v[i] = np.maximum(v[i], amp * np.exp(-(d / r_km) ** 2)) def volcanic_potential(g, tect: dict, vel, seed: int, P: dict): """0–1 per cell from hotspot chains (strongest at the active end) and plateau volcanic fields.""" tree, v = cKDTree(g.xyz), np.zeros(g.n) for h in tect.get("hotspot", []): for p, s in hotspot_track(g, h, vel, P["hotspot_spacing_km"]): _bump(v, g, tree, p, P["hotspot_km"], 1.0 - s / h["length_km"]) for pl in tect.get("plateau", []): f, vent = PL.features(pl, seed, g.radius_km), float(pl.get("vent", 1.0)) _bump(v, g, tree, latlon_to_xyz(*f["hotspot"]), 200.0, vent) for c in f["cones"]: _bump(v, g, tree, latlon_to_xyz(c["lat"], c["lon"]), c["radius_km"] + 30.0, 0.8 * vent) for c in f["calderas"]: _bump(v, g, tree, latlon_to_xyz(c["lat"], c["lon"]), c["radius_km"] + 30.0, vent) return v def run(ctx) -> dict: g = ctx.grid P = params(ctx.cfg, "seabed", DEFAULTS) z, ocean, cont, age, d_div, d_over, d_sub, t_mean, vel = ctx.need( "elevation_eroded_m", "ocean", "continental", "ocean_age_myr", "d_div_km", "d_over_km", "d_sub_km", "T_mean", "vel") z, ocean, cont = np.asarray(z, dtype=np.float64), np.asarray(ocean, bool), np.asarray(cont, bool) age, t_mean = np.asarray(age, dtype=np.float64), np.asarray(t_mean, dtype=np.float64) plateau_id = np.asarray(ctx.data.get("plateau_id", np.full(g.n, -1))) depth = np.maximum(-z, 0.0) volc = volcanic_potential(g, ctx.tect, np.asarray(vel), ctx.seed, P) ridge = np.exp(-(np.asarray(d_div) / P["ridge_km"]) ** 2) back_arc = P["back_arc"] * np.exp(-((np.asarray(d_over) - P["back_arc_km"]) / P["back_arc_width_km"]) ** 2) cluster = np.clip(0.5 + 1.5 * fbm(g.xyz, ctx.seed + 301, 4, 30.0), 0.0, 1.0) # vents come in fields vent = np.where(ocean, np.maximum.reduce([ridge, back_arc, volc]) * (0.5 + 0.5 * cluster), 0.0) d_land = distance_to(g, ~ocean) if (~ocean).any() else np.full(g.n, np.inf) t = np.full(g.n, SB_CLAY, np.int8) t[(depth < P["carbonate_depth_m"]) & (t_mean > P["carbonate_t_c"])] = SB_CARBONATE t[cont] = SB_CONTINENTAL t[d_land < P["terrigenous_km"]] = SB_TERRIGENOUS t[~cont & (age < P["young_myr"])] = SB_BASALT t[volc > P["volcanic"]] = SB_VOLCANIC t[(np.asarray(d_sub) < P["trench_km"]) & (depth > P["trench_depth_m"])] = SB_TRENCH t[vent > P["vent_field"]] = SB_VENTS t[~ocean] = SB_NONE m = np.zeros(g.n, np.int8) m[(t == SB_CLAY) & (age > 30.0) & (d_land > 1000.0) & (depth > 4000.0)] = MI_NODULES m[((t == SB_VOLCANIC) | (plateau_id >= 0)) & (depth >= 800.0) & (depth <= 2500.0)] = MI_COBALT m[cont & (depth < 500.0)] = MI_PHOSPHORITE m[vent > P["sulfides"]] = MI_SULFIDES m[~ocean] = MI_NONE deep = 1.0 + 3.0 * np.cos(np.radians(g.lat)) ** 2 # ≈ 1 °C polar … 4 °C tropical w = np.clip((P["thermocline_m"] - depth) / P["thermocline_m"], 0.0, 1.0) # shallow floors: towards the surface bottom = np.where(ocean, deep + w * (np.maximum(t_mean, -1.8) - deep), 0.0) terr = 2500.0 * np.exp(-d_land / 250.0) # aprons off the continents sed = np.where(cont, 300.0 + terr, np.minimum(5.0 * age, 800.0) + terr) # pelagic rain ≈ 5 m per Myr sed = np.where(ocean, sed, 0.0) return {"vent_potential": vent.astype(np.float32), "seabed_type": t, "seabed_mineral": m, "bottom_temp_c": bottom.astype(np.float32), "sediment_m": sed.astype(np.float32)}