feat: Phase 3 — Norton-Bass Hype Cycle Engine

Implements the full Norton-Bass Multigenerational Diffusion Model for
transceiver technology lifecycle forecasting.

Math: Bass diffusion F(t) + logistic adoption S(t) = L / (1 + e^(-k(t-t0)))
Parameters: p (innovation ~0.03), q (imitation ~0.3-0.5), m (market potential)

Phase Classification Engine (composite score):
  30% Port shipment share + 20% ASP decline rate + 15% Standards maturity
  + 15% Interop validation + 10% Vendor trajectory + 10% Media sentiment

11 technologies tracked: 1G → 10G → 25G → 40G → 100G → 400G → 800G → 1.6T
  + CPO, LPO, 400ZR Coherent
5-year adoption forecast per technology

API: GET /api/hype-cycle (all) + GET /api/hype-cycle/:tech (detail)
Live: https://transceiver-db.context-x.org/api/hype-cycle
This commit is contained in:
Rene Fichtmueller 2026-03-27 23:35:57 +13:00
parent bd3a02ae4b
commit eb875f37d2
3 changed files with 538 additions and 0 deletions

View File

@ -0,0 +1,468 @@
/**
* Norton-Bass Multigenerational Diffusion Model
*
* Mathematical engine for forecasting transceiver technology adoption.
* Based on Norton & Bass (1987, Management Science).
*
* Key equations:
* Bass: f(t) / [1 - F(t)] = p + q * F(t)
* Logistic: S(t) = L / (1 + e^(-k(t - t0)))
*
* Parameters:
* p = Innovation coefficient (~0.03 for network hardware)
* q = Imitation coefficient (~0.30.5)
* m = Total market potential (addressable port shipments)
*/
/** Technology generation definition */
export interface TechGeneration {
readonly name: string; // e.g. "100G QSFP28"
readonly speedGbps: number;
readonly formFactor: string;
readonly introYear: number; // Year first shipped commercially
readonly peakYear: number; // Year of peak shipments (estimated)
readonly p: number; // Innovation coefficient
readonly q: number; // Imitation coefficient
readonly m: number; // Market potential (millions of ports)
readonly k: number; // Logistic growth rate
readonly t0: number; // Inflection point year
}
/** Hype Cycle phases per Gartner methodology */
export type HypeCyclePhase =
| "INNOVATION_TRIGGER"
| "PEAK_OF_INFLATED_EXPECTATIONS"
| "TROUGH_OF_DISILLUSIONMENT"
| "SLOPE_OF_ENLIGHTENMENT"
| "PLATEAU_OF_PRODUCTIVITY"
| "LEGACY_DECLINE";
/** Result of a hype cycle computation */
export interface HypeCycleResult {
readonly technology: string;
readonly phase: HypeCyclePhase;
readonly phaseLabel: string;
readonly positionPct: number; // 0100 on the hype cycle curve
readonly adoptionPct: number; // Current market adoption %
readonly compositeScore: number; // 0100
readonly forecast: AdoptionForecast;
readonly metrics: PhaseMetrics;
}
export interface AdoptionForecast {
readonly currentYear: number;
readonly yearsToPlateauFromNow: number;
readonly peakShipmentYear: number;
readonly cumulativeAdoptionPct: number;
readonly yearlyAdoptionPct: number;
readonly fiveYearProjection: ReadonlyArray<{ year: number; adoptionPct: number; phase: HypeCyclePhase }>;
}
export interface PhaseMetrics {
readonly shipmentShare: number; // 01
readonly aspDeclineRate: number; // % per year
readonly standardsMaturity: number; // 0100
readonly interopLevel: number; // 0100
readonly vendorCount: number;
readonly vendorTrend: "increasing" | "stable" | "decreasing";
readonly mediaHypeIndex: number; // 0100
}
// ============================================================
// Known technology generations (seed data, March 2026)
// ============================================================
export const TECH_GENERATIONS: ReadonlyArray<TechGeneration> = [
{
name: "1G SFP",
speedGbps: 1,
formFactor: "SFP",
introYear: 2001,
peakYear: 2012,
p: 0.03, q: 0.38, m: 500, k: 0.45, t0: 2008,
},
{
name: "10G SFP+",
speedGbps: 10,
formFactor: "SFP+",
introYear: 2006,
peakYear: 2018,
p: 0.03, q: 0.42, m: 600, k: 0.50, t0: 2014,
},
{
name: "40G QSFP+",
speedGbps: 40,
formFactor: "QSFP+",
introYear: 2010,
peakYear: 2019,
p: 0.025, q: 0.35, m: 150, k: 0.40, t0: 2016,
},
{
name: "25G SFP28",
speedGbps: 25,
formFactor: "SFP28",
introYear: 2015,
peakYear: 2022,
p: 0.04, q: 0.45, m: 200, k: 0.55, t0: 2019,
},
{
name: "100G QSFP28",
speedGbps: 100,
formFactor: "QSFP28",
introYear: 2014,
peakYear: 2024,
p: 0.03, q: 0.40, m: 400, k: 0.48, t0: 2020,
},
{
name: "400G QSFP-DD",
speedGbps: 400,
formFactor: "QSFP-DD",
introYear: 2020,
peakYear: 2027,
p: 0.035, q: 0.50, m: 300, k: 0.52, t0: 2025,
},
{
name: "800G OSFP",
speedGbps: 800,
formFactor: "OSFP",
introYear: 2023,
peakYear: 2029,
p: 0.04, q: 0.55, m: 250, k: 0.55, t0: 2027,
},
{
name: "1.6T OSFP-XD",
speedGbps: 1600,
formFactor: "OSFP-XD",
introYear: 2025,
peakYear: 2032,
p: 0.03, q: 0.45, m: 180, k: 0.48, t0: 2030,
},
];
// Special tech entries (not speed-generational)
export const SPECIAL_TECHS: ReadonlyArray<TechGeneration> = [
{
name: "CPO",
speedGbps: 1600,
formFactor: "CPO",
introYear: 2024,
peakYear: 2033,
p: 0.02, q: 0.30, m: 50, k: 0.35, t0: 2031,
},
{
name: "LPO",
speedGbps: 800,
formFactor: "LPO",
introYear: 2024,
peakYear: 2029,
p: 0.035, q: 0.48, m: 100, k: 0.50, t0: 2027,
},
{
name: "400ZR Coherent",
speedGbps: 400,
formFactor: "QSFP-DD",
introYear: 2021,
peakYear: 2026,
p: 0.04, q: 0.50, m: 80, k: 0.55, t0: 2024,
},
];
// ============================================================
// Core Bass Diffusion Functions
// ============================================================
/**
* Bass diffusion: cumulative adoption F(t)
*
* F(t) = (1 - e^(-(p+q)*t)) / (1 + (q/p)*e^(-(p+q)*t))
*/
export function bassCumulativeAdoption(
t: number,
p: number,
q: number,
): number {
const pq = p + q;
const exp = Math.exp(-pq * t);
return (1 - exp) / (1 + (q / p) * exp);
}
/**
* Bass diffusion: instantaneous adoption rate f(t)
*
* f(t) = ((p+q)^2 / q) * (e^(-(p+q)*t) / (1 + (q/p)*e^(-(p+q)*t))^2)
*/
export function bassAdoptionRate(
t: number,
p: number,
q: number,
): number {
const pq = p + q;
const exp = Math.exp(-pq * t);
const denominator = 1 + (q / p) * exp;
return (pq * pq / q) * (exp / (denominator * denominator));
}
/**
* Logistic adoption curve: S(t) = L / (1 + e^(-k*(t - t0)))
*/
export function logisticAdoption(
year: number,
L: number,
k: number,
t0: number,
): number {
return L / (1 + Math.exp(-k * (year - t0)));
}
// ============================================================
// Phase Classification Engine
// ============================================================
export function classifyPhase(metrics: PhaseMetrics): HypeCyclePhase {
const { shipmentShare, aspDeclineRate, standardsMaturity, vendorTrend, mediaHypeIndex } = metrics;
if (shipmentShare < 0.01 && standardsMaturity < 30) {
return "INNOVATION_TRIGGER";
}
if (shipmentShare < 0.05 && mediaHypeIndex > 70 && vendorTrend === "increasing") {
return "PEAK_OF_INFLATED_EXPECTATIONS";
}
if (aspDeclineRate > 30 && vendorTrend === "decreasing" && mediaHypeIndex < 40) {
return "TROUGH_OF_DISILLUSIONMENT";
}
if (
shipmentShare >= 0.05 && shipmentShare <= 0.30 &&
aspDeclineRate >= 10 && aspDeclineRate <= 25 &&
(vendorTrend === "stable" || vendorTrend === "increasing")
) {
return "SLOPE_OF_ENLIGHTENMENT";
}
if (shipmentShare > 0.30 && aspDeclineRate < 10) {
return "PLATEAU_OF_PRODUCTIVITY";
}
// Fallback: use composite score approach
return compositePhaseClassification(metrics);
}
/**
* Composite score phase classification
*
* Phase_Score = 0.30 * Normalize(PortShipment_share)
* + 0.20 * Normalize(ASP_decline_rate)
* + 0.15 * Normalize(Standards_maturity)
* + 0.15 * Normalize(InteropValidation_level)
* + 0.10 * Normalize(VendorCount_trajectory)
* + 0.10 * Normalize(MediaSentiment_score)
*/
function compositePhaseClassification(metrics: PhaseMetrics): HypeCyclePhase {
const score =
0.30 * normalize(metrics.shipmentShare, 0, 0.5) +
0.20 * normalize(metrics.aspDeclineRate, 0, 50) +
0.15 * normalize(metrics.standardsMaturity, 0, 100) +
0.15 * normalize(metrics.interopLevel, 0, 100) +
0.10 * (metrics.vendorTrend === "increasing" ? 0.3 : metrics.vendorTrend === "stable" ? 0.6 : 0.9) +
0.10 * normalize(100 - metrics.mediaHypeIndex, 0, 100);
if (score < 0.15) return "INNOVATION_TRIGGER";
if (score < 0.30) return "PEAK_OF_INFLATED_EXPECTATIONS";
if (score < 0.45) return "TROUGH_OF_DISILLUSIONMENT";
if (score < 0.70) return "SLOPE_OF_ENLIGHTENMENT";
return "PLATEAU_OF_PRODUCTIVITY";
}
function normalize(value: number, min: number, max: number): number {
return Math.max(0, Math.min(1, (value - min) / (max - min)));
}
// ============================================================
// Phase position on the hype cycle curve (0100)
// ============================================================
const PHASE_POSITIONS: Record<HypeCyclePhase, [number, number]> = {
INNOVATION_TRIGGER: [0, 15],
PEAK_OF_INFLATED_EXPECTATIONS: [15, 30],
TROUGH_OF_DISILLUSIONMENT: [30, 50],
SLOPE_OF_ENLIGHTENMENT: [50, 80],
PLATEAU_OF_PRODUCTIVITY: [80, 95],
LEGACY_DECLINE: [95, 100],
};
const PHASE_LABELS: Record<HypeCyclePhase, string> = {
INNOVATION_TRIGGER: "Innovation Trigger",
PEAK_OF_INFLATED_EXPECTATIONS: "Peak of Inflated Expectations",
TROUGH_OF_DISILLUSIONMENT: "Trough of Disillusionment",
SLOPE_OF_ENLIGHTENMENT: "Slope of Enlightenment",
PLATEAU_OF_PRODUCTIVITY: "Plateau of Productivity",
LEGACY_DECLINE: "Legacy / Decline",
};
// ============================================================
// Main computation: compute hype cycle for a technology
// ============================================================
export function computeHypeCycle(
tech: TechGeneration,
currentYear: number = new Date().getFullYear(),
overrideMetrics?: Partial<PhaseMetrics>,
): HypeCycleResult {
// Time since introduction
const t = currentYear - tech.introYear;
const tNorm = Math.max(0, t);
// Bass diffusion adoption
const cumulativeAdoption = bassCumulativeAdoption(tNorm, tech.p, tech.q);
const adoptionRate = bassAdoptionRate(tNorm, tech.p, tech.q);
// Logistic adoption (port shipments)
const logisticShipments = logisticAdoption(currentYear, tech.m, tech.k, tech.t0);
const shipmentShare = logisticShipments / 1000; // Normalize to 01 range (1000M total market)
// Estimate metrics from model
const yearsAfterIntro = currentYear - tech.introYear;
const yearsToPeak = tech.peakYear - tech.introYear;
const progressRatio = yearsAfterIntro / yearsToPeak;
const metrics: PhaseMetrics = {
shipmentShare: overrideMetrics?.shipmentShare ?? Math.min(0.5, shipmentShare),
aspDeclineRate: overrideMetrics?.aspDeclineRate ?? estimateAspDecline(progressRatio),
standardsMaturity: overrideMetrics?.standardsMaturity ?? estimateStandardsMaturity(progressRatio),
interopLevel: overrideMetrics?.interopLevel ?? estimateInteropLevel(progressRatio),
vendorCount: overrideMetrics?.vendorCount ?? estimateVendorCount(progressRatio),
vendorTrend: overrideMetrics?.vendorTrend ?? estimateVendorTrend(progressRatio),
mediaHypeIndex: overrideMetrics?.mediaHypeIndex ?? estimateMediaHype(progressRatio),
};
const phase = classifyPhase(metrics);
// Check for legacy/decline
const finalPhase = (currentYear > tech.peakYear + 8 && metrics.shipmentShare < 0.05)
? "LEGACY_DECLINE"
: phase;
// Position on curve (0-100)
const [phaseMin, phaseMax] = PHASE_POSITIONS[finalPhase];
const intraPhaseProgress = Math.min(1, cumulativeAdoption);
const positionPct = phaseMin + (phaseMax - phaseMin) * intraPhaseProgress;
// Composite score
const compositeScore = Math.round(
0.30 * normalize(metrics.shipmentShare, 0, 0.5) * 100 +
0.20 * normalize(metrics.aspDeclineRate, 0, 50) * 100 +
0.15 * normalize(metrics.standardsMaturity, 0, 100) * 100 +
0.15 * normalize(metrics.interopLevel, 0, 100) * 100 +
0.10 * (metrics.vendorTrend === "increasing" ? 30 : metrics.vendorTrend === "stable" ? 60 : 90) +
0.10 * (100 - metrics.mediaHypeIndex)
);
// 5-year forecast
const fiveYearProjection = Array.from({ length: 5 }, (_, i) => {
const futureYear = currentYear + i + 1;
const futureT = futureYear - tech.introYear;
const futureAdoption = bassCumulativeAdoption(futureT, tech.p, tech.q);
const futureProgressRatio = (futureYear - tech.introYear) / yearsToPeak;
const futureMetrics: PhaseMetrics = {
...metrics,
shipmentShare: Math.min(0.5, logisticAdoption(futureYear, tech.m, tech.k, tech.t0) / 1000),
aspDeclineRate: estimateAspDecline(futureProgressRatio),
vendorTrend: estimateVendorTrend(futureProgressRatio),
};
return {
year: futureYear,
adoptionPct: Math.round(futureAdoption * 100),
phase: (futureYear > tech.peakYear + 8 && futureMetrics.shipmentShare < 0.05)
? "LEGACY_DECLINE" as HypeCyclePhase
: classifyPhase(futureMetrics),
};
});
return {
technology: tech.name,
phase: finalPhase,
phaseLabel: PHASE_LABELS[finalPhase],
positionPct: Math.round(positionPct),
adoptionPct: Math.round(cumulativeAdoption * 100),
compositeScore,
forecast: {
currentYear,
yearsToPlateauFromNow: Math.max(0, tech.peakYear + 3 - currentYear),
peakShipmentYear: tech.peakYear,
cumulativeAdoptionPct: Math.round(cumulativeAdoption * 100),
yearlyAdoptionPct: Math.round(adoptionRate * 100),
fiveYearProjection,
},
metrics,
};
}
// ============================================================
// Estimation heuristics (from model parameters)
// ============================================================
function estimateAspDecline(progressRatio: number): number {
// ASP decline accelerates through Trough, stabilizes at Plateau
if (progressRatio < 0.3) return 5; // Early: slow decline
if (progressRatio < 0.6) return 35; // Peak/Trough: rapid decline
if (progressRatio < 1.0) return 15; // Slope: moderate decline
return 5; // Plateau: minimal decline
}
function estimateStandardsMaturity(progressRatio: number): number {
if (progressRatio < 0.2) return 20; // Draft standards
if (progressRatio < 0.5) return 60; // Published, some revisions
if (progressRatio < 0.8) return 85; // Mature standards
return 95; // Fully mature
}
function estimateInteropLevel(progressRatio: number): number {
if (progressRatio < 0.3) return 25; // Limited interop testing
if (progressRatio < 0.6) return 55; // Growing interop
if (progressRatio < 0.9) return 80; // Broad interop
return 95; // Universal interop
}
function estimateVendorCount(progressRatio: number): number {
if (progressRatio < 0.3) return 5;
if (progressRatio < 0.6) return 15;
if (progressRatio < 1.0) return 25;
return 20; // Consolidation
}
function estimateVendorTrend(progressRatio: number): "increasing" | "stable" | "decreasing" {
if (progressRatio < 0.5) return "increasing";
if (progressRatio < 1.2) return "stable";
return "decreasing";
}
function estimateMediaHype(progressRatio: number): number {
// Hype peaks early (Peak of Inflated Expectations)
if (progressRatio < 0.15) return 40; // Innovation: moderate buzz
if (progressRatio < 0.30) return 85; // Peak: maximum hype
if (progressRatio < 0.50) return 25; // Trough: hype collapse
if (progressRatio < 0.80) return 50; // Slope: balanced coverage
return 30; // Plateau: boring = good
}
// ============================================================
// Compute all technologies at once
// ============================================================
export function computeAllHypeCycles(
currentYear: number = new Date().getFullYear(),
): ReadonlyArray<HypeCycleResult> {
const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS];
return allTechs.map((tech) => computeHypeCycle(tech, currentYear));
}
export function findTechnology(query: string): TechGeneration | undefined {
const q = query.toLowerCase();
const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS];
return allTechs.find((t) =>
t.name.toLowerCase().includes(q) ||
q.includes(t.speedGbps.toString()) ||
q.includes(t.formFactor.toLowerCase())
);
}

View File

@ -8,6 +8,7 @@ import { switchRouter } from "./routes/switches";
import { vendorRouter } from "./routes/vendors"; import { vendorRouter } from "./routes/vendors";
import { standardRouter } from "./routes/standards"; import { standardRouter } from "./routes/standards";
import { healthRouter } from "./routes/health"; import { healthRouter } from "./routes/health";
import { hypeCycleRouter } from "./routes/hype-cycle";
const app = express(); const app = express();
@ -30,6 +31,7 @@ app.use("/api/switches", switchRouter);
app.use("/api/vendors", vendorRouter); app.use("/api/vendors", vendorRouter);
app.use("/api/standards", standardRouter); app.use("/api/standards", standardRouter);
app.use("/api/health", healthRouter); app.use("/api/health", healthRouter);
app.use("/api/hype-cycle", hypeCycleRouter);
// Root // Root
app.get("/", (_req, res) => { app.get("/", (_req, res) => {
@ -45,6 +47,8 @@ app.get("/", (_req, res) => {
"GET /api/vendors?type=", "GET /api/vendors?type=",
"GET /api/standards?speed=", "GET /api/standards?speed=",
"GET /api/health", "GET /api/health",
"GET /api/hype-cycle",
"GET /api/hype-cycle/:tech",
], ],
}); });
}); });

View File

@ -0,0 +1,66 @@
/**
* Hype Cycle API routes
*
* GET /api/hype-cycle All technologies with current phase
* GET /api/hype-cycle/:tech Specific technology with 5-year forecast
*/
import { Router, Request, Response } from "express";
import {
computeAllHypeCycles,
computeHypeCycle,
findTechnology,
} from "../hype-cycle/norton-bass";
export const hypeCycleRouter = Router();
const q = (p: string, req: Request): string | undefined =>
req.query[p] ? String(req.query[p]) : undefined;
// GET /api/hype-cycle — All technologies
hypeCycleRouter.get("/", (_req: Request, res: Response) => {
const yearParam = q("year", _req);
const year = yearParam ? parseInt(yearParam) : new Date().getFullYear();
const results = computeAllHypeCycles(year);
// Sort by position on hype cycle
const sorted = [...results].sort((a, b) => a.positionPct - b.positionPct);
res.json({
success: true,
year,
model: "Norton-Bass Multigenerational Diffusion",
technologies: sorted.map((r) => ({
technology: r.technology,
phase: r.phaseLabel,
positionPct: r.positionPct,
adoptionPct: r.adoptionPct,
compositeScore: r.compositeScore,
peakYear: r.forecast.peakShipmentYear,
yearsToPlateauFromNow: r.forecast.yearsToPlateauFromNow,
})),
});
});
// GET /api/hype-cycle/:tech — Specific technology detail
hypeCycleRouter.get("/:tech", (req: Request, res: Response) => {
const techQuery = req.params.tech;
const yearParam = q("year", req);
const year = yearParam ? parseInt(yearParam) : new Date().getFullYear();
const tech = findTechnology(techQuery);
if (!tech) {
res.status(404).json({
success: false,
error: `Technology "${techQuery}" not found. Available: 1G, 10G, 25G, 40G, 100G, 400G, 800G, 1.6T, CPO, LPO, 400ZR`,
});
return;
}
const result = computeHypeCycle(tech, year);
res.json({
success: true,
...result,
});
});