/** * TIP Hype Cycle Engine — Norton-Bass Diffusion Model * * Fits a Bass diffusion model to historical optical transceiver shipment share data, * maps the resulting adoption curve to a Gartner Hype Cycle phase, and stores the * analysis in hype_cycle_analysis table. * * Bass model (single-generation): * cumulative adopters: F(t) = [1 - e^(-(p+q)*t)] / [1 + (q/p)*e^(-(p+q)*t)] * rate of adoption: f(t) = dF/dt = (p + q*F(t)) * (1 - F(t)) * M * * Parameters: * p — coefficient of innovation (typical: 0.001–0.03) * q — coefficient of imitation (typical: 0.10–0.50) * M — market potential as fraction of total port shipments (0–1) * * Gartner phase mapping via position on the f(t) curve relative to f(t_peak): * < 5% of M cumulative → Innovation Trigger * 5–30% rising phase → Peak of Inflated Expectations * past peak, f(t) declining, F<60% → Trough of Disillusionment * F 60–85% → Slope of Enlightenment * F > 85% → Plateau of Productivity * * Sources for seed data: * Dell'Oro Group (public summaries), LightCounting quarterly (free tier), * CIR Research, company earnings calls (public). */ import { pool } from "./db"; // ── Historical seed data ─────────────────────────────────────────────────────── // Approximate shipment share (% of total optical transceiver port volume) per year. // Derived from publicly available Dell'Oro / LightCounting industry summaries. // These are best estimates — the engine will fit curves to these points. interface HistoricalPoint { year: number; shipmentShare: number; // 0.0 – 1.0 fraction of total port shipments aspUsd?: number; // OEM ASP at that year (optional, for price trajectory) } const HISTORICAL_DATA: Record = { "10G-SFP+": [ { year: 2010, shipmentShare: 0.05, aspUsd: 800 }, { year: 2011, shipmentShare: 0.12, aspUsd: 600 }, { year: 2012, shipmentShare: 0.22, aspUsd: 450 }, { year: 2013, shipmentShare: 0.32, aspUsd: 320 }, { year: 2014, shipmentShare: 0.42, aspUsd: 240 }, { year: 2015, shipmentShare: 0.52, aspUsd: 180 }, { year: 2016, shipmentShare: 0.60, aspUsd: 130 }, { year: 2017, shipmentShare: 0.62, aspUsd: 90 }, { year: 2018, shipmentShare: 0.58, aspUsd: 60 }, { year: 2019, shipmentShare: 0.50, aspUsd: 40 }, { year: 2020, shipmentShare: 0.42, aspUsd: 28 }, { year: 2021, shipmentShare: 0.36, aspUsd: 20 }, { year: 2022, shipmentShare: 0.30, aspUsd: 14 }, { year: 2023, shipmentShare: 0.24, aspUsd: 10 }, { year: 2024, shipmentShare: 0.18, aspUsd: 7 }, { year: 2025, shipmentShare: 0.13, aspUsd: 5 }, ], "100G-QSFP28": [ { year: 2016, shipmentShare: 0.02, aspUsd: 3200 }, { year: 2017, shipmentShare: 0.08, aspUsd: 2200 }, { year: 2018, shipmentShare: 0.18, aspUsd: 1400 }, { year: 2019, shipmentShare: 0.28, aspUsd: 850 }, { year: 2020, shipmentShare: 0.36, aspUsd: 480 }, { year: 2021, shipmentShare: 0.42, aspUsd: 280 }, { year: 2022, shipmentShare: 0.44, aspUsd: 160 }, { year: 2023, shipmentShare: 0.42, aspUsd: 95 }, { year: 2024, shipmentShare: 0.38, aspUsd: 60 }, { year: 2025, shipmentShare: 0.34, aspUsd: 40 }, ], "400G-QSFP-DD": [ { year: 2020, shipmentShare: 0.01, aspUsd: 12000 }, { year: 2021, shipmentShare: 0.04, aspUsd: 7500 }, { year: 2022, shipmentShare: 0.09, aspUsd: 4200 }, { year: 2023, shipmentShare: 0.18, aspUsd: 2200 }, { year: 2024, shipmentShare: 0.28, aspUsd: 1100 }, { year: 2025, shipmentShare: 0.36, aspUsd: 600 }, ], "800G-OSFP": [ { year: 2023, shipmentShare: 0.005, aspUsd: 24000 }, { year: 2024, shipmentShare: 0.025, aspUsd: 16000 }, { year: 2025, shipmentShare: 0.07, aspUsd: 10000 }, ], "400G-ZR": [ { year: 2021, shipmentShare: 0.005, aspUsd: 8500 }, { year: 2022, shipmentShare: 0.015, aspUsd: 5500 }, { year: 2023, shipmentShare: 0.030, aspUsd: 3800 }, { year: 2024, shipmentShare: 0.050, aspUsd: 2800 }, { year: 2025, shipmentShare: 0.075, aspUsd: 2100 }, ], "1.6T-OSFP": [ { year: 2025, shipmentShare: 0.001, aspUsd: 40000 }, ], }; // Year the technology first entered commercial availability (t=0 for Bass model) const FIRST_YEAR: Record = { "10G-SFP+": 2009, "100G-QSFP28": 2015, "400G-QSFP-DD": 2019, "800G-OSFP": 2022, "400G-ZR": 2020, "1.6T-OSFP": 2024, }; // Estimated peak market share (M) — as fraction of all optical transceiver ports at peak // Technologies plateau at different shares because of market stratification const MARKET_POTENTIAL: Record = { "10G-SFP+": 0.65, // Was dominant, now declining "100G-QSFP28": 0.48, // Plateaued, being replaced by 400G "400G-QSFP-DD": 0.42, // Growing, will peak ~2027 "800G-OSFP": 0.35, // Just starting, will peak ~2028-2029 "400G-ZR": 0.15, // Coherent niche, smaller market "1.6T-OSFP": 0.30, // Future generation, estimate only }; // ── Bass model math ──────────────────────────────────────────────────────────── /** Bass cumulative adoption fraction F(t) — 0..1 relative to M */ function bassCumulative(p: number, q: number, t: number): number { if (t <= 0) return 0; const pq = p + q; const num = 1 - Math.exp(-pq * t); const den = 1 + (q / p) * Math.exp(-pq * t); return num / den; } /** Bass instantaneous adoption rate f(t) = dF/dt * M */ function bassRate(p: number, q: number, M: number, t: number): number { const F = bassCumulative(p, q, t); return (p + q * F) * (1 - F) * M; } /** Year at which Bass adoption rate peaks: t_peak = ln(q/p) / (p+q) */ function bassPeakTime(p: number, q: number): number { if (q <= p) return 0; // no peak (monotonically declining from t=0, unusual) return Math.log(q / p) / (p + q); } // ── Least-squares Bass fitting ──────────────────────────────────────────────── interface BassParams { p: number; q: number; M: number; rSquared: number } /** * Fit Bass parameters (p, q) to historical shipment share data via gradient descent. * M is fixed (from MARKET_POTENTIAL) to reduce the search space. */ function fitBass( data: HistoricalPoint[], firstYear: number, M: number ): BassParams { if (data.length < 2) { // Not enough data — return reasonable defaults return { p: 0.01, q: 0.25, M, rSquared: 0 }; } // Convert to t (years since first year) and normalised share (relative to M) const points = data.map((d) => ({ t: d.year - firstYear, F: Math.min(d.shipmentShare / M, 0.999), // cumulative fraction of M })); // Grid search over reasonable p/q ranges, then refine let bestP = 0.01; let bestQ = 0.25; let bestSse = Infinity; const pRange = [0.001, 0.003, 0.005, 0.008, 0.01, 0.015, 0.02, 0.03]; const qRange = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.50]; for (const p of pRange) { for (const q of qRange) { let sse = 0; for (const pt of points) { const predicted = bassCumulative(p, q, pt.t); sse += (predicted - pt.F) ** 2; } if (sse < bestSse) { bestSse = sse; bestP = p; bestQ = q; } } } // Fine-grained refinement around best grid point const step = 0.001; for (let dp = -5; dp <= 5; dp++) { for (let dq = -10; dq <= 10; dq++) { const p = Math.max(0.001, bestP + dp * step); const q = Math.max(0.01, bestQ + dq * step); let sse = 0; for (const pt of points) { const predicted = bassCumulative(p, q, pt.t); sse += (predicted - pt.F) ** 2; } if (sse < bestSse) { bestSse = sse; bestP = p; bestQ = q; } } } // Compute R² const meanF = points.reduce((s, pt) => s + pt.F, 0) / points.length; const ssTot = points.reduce((s, pt) => s + (pt.F - meanF) ** 2, 0); const rSquared = ssTot > 0 ? Math.max(0, 1 - bestSse / ssTot) : 0; return { p: bestP, q: bestQ, M, rSquared }; } // ── Gartner Hype Cycle phase detection ─────────────────────────────────────── type HypePhase = | "innovation_trigger" | "peak_inflated_expectations" | "trough_disillusionment" | "slope_enlightenment" | "plateau_productivity"; interface PhaseResult { phase: HypePhase; hypeScore: number; // 0–100 position on Gartner curve yearsToNext: number; // estimated years until next phase phaseSinceYear: number; // approximate calendar year current phase started } /** * Map Bass model position to Gartner Hype Cycle phase. * Uses cumulative adoption F(t) and whether we're before/after the peak. */ function detectPhase( p: number, q: number, M: number, currentT: number, firstYear: number ): PhaseResult { const tPeak = bassPeakTime(p, q); const F = bassCumulative(p, q, currentT); const currentShare = F * M; const peakF = bassCumulative(p, q, tPeak); // Normalised position: 0 = start, 1 = peak rate (not peak cumulative) const prePeakNorm = tPeak > 0 ? Math.min(currentT / tPeak, 1) : 1; const isPrePeak = currentT < tPeak; let phase: HypePhase; let hypeScore: number; let yearsToNext: number; let phaseSinceYear: number; if (currentShare < 0.03 * M) { // < 3% of market potential — innovation trigger phase = "innovation_trigger"; hypeScore = Math.round(prePeakNorm * 25); // Estimate time to reach 5% share let tTo5pct = currentT + 1; while (bassCumulative(p, q, tTo5pct) * M < 0.05 * M && tTo5pct < 30) tTo5pct++; yearsToNext = Math.max(0.5, tTo5pct - currentT); phaseSinceYear = firstYear; } else if (isPrePeak || F < peakF * 0.85) { // Rising phase — Peak of Inflated Expectations phase = "peak_inflated_expectations"; // Score 25–75 based on how close we are to the rate peak hypeScore = Math.round(25 + prePeakNorm * 50); yearsToNext = Math.max(0.5, tPeak - currentT + 1); // Phase started when share crossed 5% let tStart = 0; while (bassCumulative(p, q, tStart) * M < 0.03 * M && tStart < tPeak) tStart += 0.5; phaseSinceYear = Math.round(firstYear + tStart); } else if (F < 0.50) { // Post-peak, still < 50% adoption — Trough of Disillusionment phase = "trough_disillusionment"; // Score 75 → 45 (declining from peak) const postPeakProgress = (F - peakF * 0.85) / (0.50 - peakF * 0.85); hypeScore = Math.round(75 - postPeakProgress * 30); // Time to reach 50% share let tTo50 = currentT + 0.5; while (bassCumulative(p, q, tTo50) < 0.50 && tTo50 < 30) tTo50 += 0.5; yearsToNext = Math.max(0.5, tTo50 - currentT); phaseSinceYear = Math.round(firstYear + tPeak); } else if (F < 0.85) { // 50–85% cumulative — Slope of Enlightenment phase = "slope_enlightenment"; const enlightProgress = (F - 0.50) / (0.85 - 0.50); hypeScore = Math.round(45 + enlightProgress * 30); // Time to reach 85% share let tTo85 = currentT + 0.5; while (bassCumulative(p, q, tTo85) < 0.85 && tTo85 < 30) tTo85 += 0.5; yearsToNext = Math.max(0.5, tTo85 - currentT); let tStart50 = currentT; while (tStart50 > 0 && bassCumulative(p, q, tStart50 - 0.5) >= 0.50) tStart50 -= 0.5; phaseSinceYear = Math.round(firstYear + tStart50); } else { // > 85% adoption — Plateau of Productivity phase = "plateau_productivity"; hypeScore = Math.min(100, Math.round(75 + (F - 0.85) / (1 - 0.85) * 25)); yearsToNext = 0; // no next phase let tStart85 = currentT; while (tStart85 > 0 && bassCumulative(p, q, tStart85 - 0.5) < 0.85) tStart85 += 0.5; phaseSinceYear = Math.round(firstYear + tStart85); } return { phase, hypeScore, yearsToNext, phaseSinceYear }; } // ── ASP projection ──────────────────────────────────────────────────────────── /** * Project ASP decline based on historical data (log-linear regression). * Returns: current ASP (estimated) + % decline expected over 3 years. */ function projectAsp(data: HistoricalPoint[]): { currentAsp: number | null; declinePct3y: number | null } { const withAsp = data.filter((d) => d.aspUsd != null && d.aspUsd > 0); if (withAsp.length < 2) return { currentAsp: withAsp[0]?.aspUsd ?? null, declinePct3y: null }; // Log-linear fit: ln(asp) = a + b * year const n = withAsp.length; const sumX = withAsp.reduce((s, d) => s + d.year, 0); const sumY = withAsp.reduce((s, d) => s + Math.log(d.aspUsd!), 0); const sumXY = withAsp.reduce((s, d) => s + d.year * Math.log(d.aspUsd!), 0); const sumX2 = withAsp.reduce((s, d) => s + d.year * d.year, 0); const b = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); const a = (sumY - b * sumX) / n; const currentYear = new Date().getFullYear(); const currentAsp = Math.exp(a + b * currentYear); const futureAsp = Math.exp(a + b * (currentYear + 3)); const declinePct3y = ((currentAsp - futureAsp) / currentAsp) * 100; return { currentAsp: Math.round(currentAsp), declinePct3y: Math.round(declinePct3y), }; } // ── Seed market_metrics with historical data ────────────────────────────────── async function seedMarketMetrics(): Promise { const currentYear = new Date().getFullYear(); for (const [tech, points] of Object.entries(HISTORICAL_DATA)) { for (const pt of points) { if (pt.year > currentYear) continue; // Don't seed future projections // shipment_share await pool.query( `INSERT INTO market_metrics (time, technology, metric_type, value, source, notes) VALUES ($1, $2, 'shipment_share', $3, $4, $5) ON CONFLICT DO NOTHING`, [ new Date(pt.year, 6, 1).toISOString(), // mid-year timestamp tech, pt.shipmentShare, "Dell\'Oro/LightCounting (public summary)", `Annual shipment share ${pt.year}`, ] ); // asp_usd if (pt.aspUsd) { await pool.query( `INSERT INTO market_metrics (time, technology, metric_type, value, source, notes) VALUES ($1, $2, 'asp_usd', $3, $4, $5) ON CONFLICT DO NOTHING`, [ new Date(pt.year, 6, 1).toISOString(), tech, pt.aspUsd, "Dell\'Oro/LightCounting (public summary)", `OEM ASP estimate ${pt.year} USD`, ] ); } } } } // ── Main compute function ───────────────────────────────────────────────────── export async function computeHypeCycle(): Promise { console.log("=== Hype Cycle Engine — Norton-Bass Model ===\n"); // Seed historical data if not present const { rows: existing } = await pool.query( `SELECT COUNT(*) AS n FROM market_metrics WHERE metric_type = 'shipment_share'` ); if (parseInt(existing[0].n, 10) < 10) { console.log(" Seeding historical market_metrics data..."); await seedMarketMetrics(); console.log(" Seed complete.\n"); } const currentYear = new Date().getFullYear(); const results: Array<{ technology: string; phase: HypePhase; hypeScore: number; currentShare: number; aspCurrent: number | null; aspDecline3y: number | null; }> = []; for (const [tech, data] of Object.entries(HISTORICAL_DATA)) { const firstYear = FIRST_YEAR[tech] ?? (data[0]?.year ?? 2020); const M = MARKET_POTENTIAL[tech] ?? 0.30; const currentT = currentYear - firstYear; // Fit Bass model const { p, q, rSquared } = fitBass(data, firstYear, M); const tPeak = bassPeakTime(p, q); const F = bassCumulative(p, q, currentT); const currentShare = F * M; // Detect phase const { phase, hypeScore, yearsToNext, phaseSinceYear } = detectPhase( p, q, M, currentT, firstYear ); // Project 1y and 3y adoption const F1y = bassCumulative(p, q, currentT + 1); const F3y = bassCumulative(p, q, currentT + 3); // ASP projection const { currentAsp, declinePct3y } = projectAsp(data); // Store result in hype_cycle_analysis await pool.query( `INSERT INTO hype_cycle_analysis ( computed_at, technology, bass_p, bass_q, bass_m, t_peak_year, current_t, current_share, projected_share_1y, projected_share_3y, hype_phase, hype_score, phase_since_year, years_to_next_phase, asp_current_usd, asp_decline_pct_3y, r_squared, data_points, notes ) VALUES ( NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 )`, [ tech, p, q, M, Math.round((firstYear + tPeak) * 10) / 10, currentT, Math.round(currentShare * 1000) / 1000, Math.round(F1y * M * 1000) / 1000, Math.round(F3y * M * 1000) / 1000, phase, hypeScore, phaseSinceYear, Math.round(yearsToNext * 10) / 10, currentAsp, declinePct3y, Math.round(rSquared * 1000) / 1000, data.length, `Fitted via grid search + refinement. Data points: ${data[0]?.year}–${data[data.length - 1]?.year}`, ] ); results.push({ technology: tech, phase, hypeScore, currentShare, aspCurrent: currentAsp, aspDecline3y: declinePct3y }); const phaseEmoji: Record = { innovation_trigger: "🔬", peak_inflated_expectations: "📈", trough_disillusionment: "📉", slope_enlightenment: "🔄", plateau_productivity: "✅", }; console.log( ` ${phaseEmoji[phase]} ${tech.padEnd(18)} ` + `phase=${phase.padEnd(30)} ` + `score=${hypeScore.toString().padStart(3)} ` + `share=${(currentShare * 100).toFixed(1).padStart(5)}% ` + `R²=${rSquared.toFixed(2)} ` + `ASP=${currentAsp ? `$${currentAsp.toLocaleString()}` : "n/a"}` ); } // Update hype_score in market_metrics for API consumers for (const r of results) { await pool.query( `INSERT INTO market_metrics (time, technology, metric_type, value, source, notes) VALUES (NOW(), $1, 'hype_score', $2, 'hype-cycle-engine', $3)`, [r.technology, r.hypeScore, r.phase] ); } // Cleanup old analysis rows await pool.query(`SELECT cleanup_hype_cycle_analysis()`); console.log(`\n=== Hype Cycle Engine Complete — ${results.length} technologies analyzed ===\n`); } // ── Export for API endpoint ─────────────────────────────────────────────────── /** Fetch latest hype cycle analysis for all or a specific technology */ export async function getHypeCycleAnalysis(technology?: string): Promise { const sql = technology ? `SELECT * FROM hype_cycle_analysis WHERE technology = $1 ORDER BY computed_at DESC LIMIT 1` : `SELECT DISTINCT ON (technology) * FROM hype_cycle_analysis ORDER BY technology, computed_at DESC`; const { rows } = await pool.query(sql, technology ? [technology] : []); return rows; } // ── CLI entry point ─────────────────────────────────────────────────────────── if (require.main === module) { computeHypeCycle() .then(() => pool.end()) .catch((err: unknown) => { console.error("Fatal:", err); pool.end(); process.exit(1); }); }