fuzzyworld

shadow-zone / commit

Add five impossible metamaterial core presets

e86a2c693b73632c1682bd52abc1605dae1cf5ac · Pavel Safronov · 2026-06-13 15:20:35 -0700 · parents c37857b7b33a

Add five impossible metamaterial core presets

Each preset is driven by a coreModifier flag on EarthModel that
traceRay reads at CMB boundary crossings:

- Negative Refraction: Snell's law negated at CMB (thetaSign flips),
  ray bends to the same side of the normal as the incident ray
- Zero-Index: all transmitted rays forced radial (forceRadial=true,
  cosI=1, dtheta=0); bounces off centre and exits the same CMB point
- Spiral Trap: v(r)=v₀·r/R_CMB profile; sinI is constant so rays
  spiral inward indefinitely — a seismic black hole
- Retrograde: outerCoreVTop > outerCoreVBottom (v decreases with
  depth); v/r falls going inward so rays TIR at the ICB velocity jump
- Perfect Absorber: all waves blocked at CMB entry; pure geometric
  shadow with no curved edges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 README.md        | 12 ++++++++++
 src/earth.ts     | 48 +++++++++++++++++++++++++++++++++----
 src/rayTracer.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++----------
 3 files changed, 115 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 6b6257b..4e23c0a 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,18 @@ Core*, *Giant Core*, *Solid Earth* — to explore non-Earth-like scenarios.
 - Velocity-based layer colouring — equal speed → equal colour
 - Hover over any red arc on the surface to see the exact shadow zone range
 - Collapsible description and Wikipedia reference in the sidebar
+- **Impossible core presets** — five physically impossible metamaterial-style
+  cores (marked ⚠️) that go beyond what sliders alone can express:
+  - *Negative Refraction* — Snell's law negated at the CMB; rays bend to the
+    same side of the normal as the incident ray
+  - *Zero-Index Core* — all transmitted rays forced radial; core acts as a
+    perfect converging lens with no angular phase accumulation
+  - *Spiral Trap* — v∝r profile (zero at centre); sinI is constant, so rays
+    spiral inward indefinitely — a seismic black hole
+  - *Retrograde Core* — velocity decreases with depth; rays reach the ICB
+    and total-internally-reflect, expanding the shadow zone
+  - *Perfect Absorber* — all waves absorbed at the CMB; shadow zone equals
+    the pure geometric shadow of the core disk
 
 ## Tech stack
 
diff --git a/src/earth.ts b/src/earth.ts
index 464cdf7..27c4e47 100644
--- a/src/earth.ts
+++ b/src/earth.ts
@@ -24,6 +24,8 @@ export interface EarthLayer {
 export interface EarthModel {
   radius: number;
   layers: EarthLayer[];
+  coreModifier: string;  // '' | 'negative-refraction' | 'zero-index' | 'absorber'
+  cmbRadius: number;     // outer core radius (km), used by ray tracer for modifier checks
   vAt(r: number, wave: 'P' | 'S'): number;
   layerAt(r: number): EarthLayer;
 }
@@ -65,18 +67,23 @@ export function buildEarthModel(state: AppState): EarthModel {
   // ── Outer core ───────────────────────────────────────────────────────────
   // Velocity varies linearly from outerCoreVBottom at r=icr to outerCoreVTop at r=CMB.
   // Treated as liquid (vS=0) when outerCoreVTop < 10 km/s; solid otherwise.
+  // Exception: spiral-trap uses v(r) = outerCoreVTop * (r/CMB) — zero at center,
+  // producing constant sinI = p*v₀/CMB so rays spiral inward without turning.
   const ocRange = CMB - icr;
   const ocMidV = ocRange <= 0 ? outerCoreVTop
     : (outerCoreVBottom + outerCoreVTop) / 2;
   const ocIsSolid = outerCoreVTop >= 10;
+  const isSpiralTrap = state.coreModifier === 'spiral-trap';
   layers.push({
     rMin: icr,
     rMax: CMB,
-    vP: (r) => {
-      if (ocRange <= 0) return outerCoreVTop;
-      const t = (r - icr) / ocRange; // 0 at inner boundary, 1 at CMB
-      return outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom);
-    },
+    vP: isSpiralTrap
+      ? (r) => Math.max(0.001, outerCoreVTop * (r / CMB))
+      : (r) => {
+          if (ocRange <= 0) return outerCoreVTop;
+          const t = (r - icr) / ocRange; // 0 at inner boundary, 1 at CMB
+          return outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom);
+        },
     vS: ocIsSolid
       ? (r) => {
           if (ocRange <= 0) return outerCoreVTop * 0.6;
@@ -140,6 +147,8 @@ export function buildEarthModel(state: AppState): EarthModel {
   const model: EarthModel = {
     radius: R_EARTH,
     layers,
+    coreModifier: state.coreModifier,
+    cmbRadius: CMB,
     vAt(r: number, wave: 'P' | 'S'): number {
       const layer = this.layerAt(r);
       return wave === 'P' ? layer.vP(r) : layer.vS(r);
@@ -168,6 +177,7 @@ const BASE: AppState = {
   numRays: 60,
   preset: 'earth',
   earthquakeId: '',
+  coreModifier: '',
 };
 
 export const PRESETS: Record<string, AppState> = {
@@ -186,4 +196,32 @@ export const PRESETS: Record<string, AppState> = {
   'giant-core': { ...BASE, outerCoreRadius: 5000, innerCoreRadius: 1220, preset: 'giant-core' },
   'tiny-core': { ...BASE, outerCoreRadius: 500, innerCoreRadius: 200, preset: 'tiny-core' },
   'fast-core': { ...BASE, outerCoreVTop: 15.0, outerCoreVBottom: 14.0, innerCoreV: 14.5, preset: 'fast-core' },
+
+  // ── Impossible / metamaterial cores ─────────────────────────────────────
+  // Ray tracer interprets model.coreModifier to apply non-standard boundary physics.
+
+  // Acoustic metamaterial with negative effective bulk modulus and density.
+  // Snell's law is negated at the CMB: transmitted ray bends to the same side
+  // of the normal as the incident ray. Angular direction reverses inside the core.
+  'neg-refraction': { ...BASE, coreModifier: 'negative-refraction', preset: 'neg-refraction' },
+
+  // v_core → ∞ (zero-index medium): phase velocity is infinite, so sin(θ_t) → 0
+  // for all incident angles. Every transmitted ray is forced radial inside the core
+  // with no angular accumulation. The core acts as a perfect converging lens.
+  'zero-index': { ...BASE, coreModifier: 'zero-index', preset: 'zero-index' },
+
+  // v(r) = v₀·(r/R_CMB): velocity → 0 at centre. Snell parameter sinI = p·v₀/R_CMB
+  // is constant, so rays spiral inward indefinitely — a seismic black hole.
+  // Inner core removed (innerCoreRadius: 0) so the v∝r profile extends to r=0.
+  'spiral-trap': { ...BASE, innerCoreRadius: 0, coreModifier: 'spiral-trap', preset: 'spiral-trap' },
+
+  // Retrograde velocity gradient: v decreases with depth (opposite real Earth).
+  // outerCoreVTop (at CMB) > outerCoreVBottom (at ICB) → v/r falls going inward,
+  // so rays never reach a turning point and are TIR'd at the ICB velocity jump.
+  // The core acts as a concave mirror; the shadow zone expands.
+  'retrograde': { ...BASE, outerCoreVTop: 8.0, outerCoreVBottom: 2.0, coreModifier: '', preset: 'retrograde' },
+
+  // All waves absorbed at the CMB — nothing penetrates the core.
+  // Shadow zone is the pure geometric shadow of the core disk.
+  'absorber': { ...BASE, coreModifier: 'absorber', preset: 'absorber' },
 };
diff --git a/src/rayTracer.ts b/src/rayTracer.ts
index ac47935..e0ddc5b 100644
--- a/src/rayTracer.ts
+++ b/src/rayTracer.ts
@@ -36,6 +36,12 @@ export function traceRay(
   let theta = 0;
   let sign = -1; // -1 = going down
 
+  // Impossible-core modifier state
+  // thetaSign: +1 normal angular direction, -1 for negative-refraction inside core
+  // forceRadial: true inside zero-index core — ray travels radially, no θ accumulation
+  let thetaSign = 1;
+  let forceRadial = false;
+
   const v0 = model.vAt(r, wave);
   if (v0 === 0) {
     return { points, rayParameter: p, takeoffAngle, epicentralDistance: null, travelTimeSec: null, wave, blocked: false };
@@ -62,16 +68,17 @@ export function traceRay(
 
     const sinI = p * v / r;
 
-    if (sinI >= 1) {
+    // For zero-index core the ray is forced radial — skip the Bouguer turning check.
+    if (!forceRadial && sinI >= 1) {
       sign = 1;
     }
 
-    // cosI → 0 at turning point (sinI = 1). Without a minimum, dr = 0 forever.
-    // A tiny minimum upward velocity lets the ray escape the turning point.
     const rawCosI = Math.sqrt(Math.max(0, 1 - sinI * sinI));
-    const cosI = sinI >= 1 ? Math.max(rawCosI, 0.001) : rawCosI;
+    // Zero-index core: ray is perfectly radial regardless of ray parameter.
+    const cosI = forceRadial ? 1 : (sinI >= 1 ? Math.max(rawCosI, 0.001) : rawCosI);
     const dr = sign * cosI * STEP;
-    const dtheta = (p * v / (r * r)) * STEP;
+    // Zero-index: no angular accumulation; negative-refraction: reverse angular direction.
+    const dtheta = forceRadial ? 0 : thetaSign * (p * v / (r * r)) * STEP;
 
     const rNext = r + dr;
     const layerNow = model.layerAt(r);
@@ -82,13 +89,12 @@ export function traceRay(
 
     if (layerNow !== layerNext) {
       const bR = sign < 0 ? layerNow.rMin : layerNow.rMax;
+      const isCMB = model.cmbRadius > 0 && Math.abs(bR - model.cmbRadius) < 2;
 
       const vOld = model.vAt(bR - 0.001 * sign, wave);
       const vNew = model.vAt(bR + 0.001 * sign, wave);
 
-      const sinIBound = p * vOld / bR;
-      const sinINew = sinIBound * (vNew / vOld);
-
+      // S-wave absorbed by liquid outer core
       if (wave === 'S' && vNew === 0) {
         const fracStep = Math.abs(bR - r) / (Math.abs(dr) + 1e-10);
         travelTimeSec += fracStep * STEP / v;
@@ -97,9 +103,38 @@ export function traceRay(
         break;
       }
 
-      if (Math.abs(sinINew) > 1) {
-        newSign = -sign;
-        newR = r + newSign * cosI * STEP;
+      // Perfect absorber: all waves stopped at CMB
+      if (isCMB && sign < 0 && model.coreModifier === 'absorber') {
+        const fracStep = Math.abs(bR - r) / (Math.abs(dr) + 1e-10);
+        travelTimeSec += fracStep * STEP / v;
+        points.push({ r: bR, theta: theta + dtheta * fracStep });
+        blocked = true;
+        break;
+      }
+
+      // Normal Snell's law TIR check — skipped inside zero-index core since the
+      // ray is perfectly radial (angle = 0) and passes through any inner boundary.
+      let didTIR = false;
+      if (!forceRadial) {
+        const sinIBound = p * vOld / bR;
+        const sinINew = sinIBound * (vNew / vOld);
+        if (Math.abs(sinINew) > 1) {
+          newSign = -sign;
+          newR = r + newSign * cosI * STEP;
+          didTIR = true;
+        }
+      }
+
+      // CMB impossible-core modifiers (only when ray actually crosses, not TIR'd)
+      if (isCMB && !didTIR) {
+        if (model.coreModifier === 'negative-refraction') {
+          // Negate tangential component: flip angular direction on each CMB crossing.
+          thetaSign = sign < 0 ? -1 : 1;
+        }
+        if (model.coreModifier === 'zero-index') {
+          // Inside core: radial-only travel. Restore normal Bouguer on exit.
+          forceRadial = sign < 0;
+        }
       }
     }
 
@@ -112,11 +147,23 @@ export function traceRay(
     if (r >= R_EARTH) {
       r = R_EARTH;
       points.push({ r, theta });
-      epicentralDistance = (theta * 180) / Math.PI;
+      // Use absolute value: negative theta means the ray curved backward
+      // (negative refraction), but distance is always a positive angle.
+      epicentralDistance = Math.abs((theta * 180) / Math.PI);
       break;
     }
 
-    if (r <= 1) break;
+    if (r <= 1) {
+      if (forceRadial) {
+        // Zero-index core: bounce off centre and head back out radially.
+        sign = 1;
+        r = 2;
+        continue;
+      }
+      // Spiral-trap and other rays that reach the centre are absorbed.
+      blocked = true;
+      break;
+    }
   }
 
   return {