Buckets:
| from __future__ import annotations | |
| import csv | |
| import math | |
| import textwrap | |
| import zipfile | |
| from pathlib import Path | |
| from typing import Any | |
| import matplotlib.pyplot as plt | |
| from local_hardware_items import ( | |
| catalog_from_items, | |
| load_items, | |
| load_memory_price_sources, | |
| ) | |
| plt.rcParams["svg.hashsalt"] = "local-hardware-frontier" | |
| ROOT = Path(__file__).resolve().parents[1] | |
| DATA_DIR = ROOT / "data" | |
| ITEMS_DIR = DATA_DIR / "items" | |
| CHARTS_DIR = ROOT / "charts" | |
| MONOTONIC_CHARTS_DIR = CHARTS_DIR / "monotonic" | |
| PRICE_PER_MEMORY_CHARTS_DIR = CHARTS_DIR / "price_per_memory" | |
| BANDWIDTH_CHARTS_DIR = CHARTS_DIR / "bandwidth" | |
| PRICE_PER_MEMORY_BEST_CHARTS_DIR = CHARTS_DIR / "best_so_far_price_per_memory" | |
| BANDWIDTH_BEST_CHARTS_DIR = CHARTS_DIR / "best_so_far_bandwidth" | |
| PROJECTION_CHARTS_DIR = CHARTS_DIR / "projections" | |
| GALLERY_DIR = ROOT / "gallery" | |
| for directory in ( | |
| DATA_DIR, | |
| CHARTS_DIR, | |
| MONOTONIC_CHARTS_DIR, | |
| PRICE_PER_MEMORY_CHARTS_DIR, | |
| BANDWIDTH_CHARTS_DIR, | |
| PRICE_PER_MEMORY_BEST_CHARTS_DIR, | |
| BANDWIDTH_BEST_CHARTS_DIR, | |
| PROJECTION_CHARTS_DIR, | |
| GALLERY_DIR, | |
| ): | |
| directory.mkdir(parents=True, exist_ok=True) | |
| YEARS = list(range(2020, 2027)) | |
| PRODUCERS = ["Apple", "NVIDIA", "AMD"] | |
| CONSUMER_PROJECTION_BANDS = [ | |
| ("under_3000", "Under 3k", "#2563eb"), | |
| ("3000_5000", "3k-5k", "#059669"), | |
| ("5000_10000", "5k-10k", "#dc2626"), | |
| ] | |
| BANDS = [ | |
| ("under_3000", "<$3,000", 0, 3000), | |
| ("3000_5000", "$3,000–$5,000", 3000, 5000), | |
| ("5000_10000", "$5,000–$10,000", 5000, 10000), | |
| ("10000_25000", "$10,000–$25,000", 10000, 25000), | |
| ("25000_75000", "$25,000–$75,000", 25000, 75000), | |
| ("75000_plus", "$75,000+", 75000, None), | |
| ] | |
| EVIDENCE_RANK = {"quoted": 3, "reconstructed": 2, "bom_inferred": 1} | |
| # U = unified/coherent memory; V = one discrete GPU's VRAM; | |
| # Σ = aggregate VRAM across multiple GPUs in one chassis. | |
| # | |
| # data/items/*.json is the editable source of truth. Each price_history | |
| # entry expands into one catalog row. | |
| items: list[dict[str, Any]] = load_items(ITEMS_DIR, bands=BANDS) | |
| catalog: list[dict[str, Any]] = catalog_from_items(items) | |
| memory_price_rows: list[dict[str, Any]] = load_memory_price_sources( | |
| DATA_DIR / "memory_price_sources.csv", | |
| items, | |
| ) | |
| # Validate band assignments against price. | |
| band_map = {key: (lo, hi) for key, _, lo, hi in BANDS} | |
| for row in catalog: | |
| lo, hi = band_map[row["price_band"]] | |
| p = row["price_usd"] | |
| assert p >= lo, (row["system"], p, row["price_band"]) | |
| if hi is not None: | |
| assert p < hi, (row["system"], p, row["price_band"]) | |
| # Derive the yearly maximum for every producer and price band. | |
| frontier: list[dict[str, Any]] = [] | |
| for band_key, band_label, _, _ in BANDS: | |
| for year in YEARS: | |
| for producer in PRODUCERS: | |
| candidates = [ | |
| r for r in catalog | |
| if r["price_band"] == band_key | |
| and r["producer"] == producer | |
| and r["start_year"] <= year <= r["end_year"] | |
| ] | |
| if not candidates: | |
| frontier.append(dict( | |
| year=year, price_band=band_key, price_band_label=band_label, | |
| producer=producer, memory_gb="", memory_type="", symbol="", | |
| system="", price_usd="", price_kind="", evidence="", | |
| confidence="", source_url="", note="", item_id="", | |
| price_event_id="", bandwidth_gbps="", bandwidth_source_url="", | |
| bandwidth_note="", price_per_memory_gb="" | |
| )) | |
| continue | |
| # Maximum memory; for ties prefer directly quoted complete systems. | |
| best = max(candidates, key=lambda r: (r["memory_gb"], EVIDENCE_RANK[r["evidence"]])) | |
| frontier.append(dict( | |
| year=year, price_band=band_key, price_band_label=band_label, | |
| producer=producer, memory_gb=best["memory_gb"], | |
| memory_type=best["memory_type"], symbol=best["symbol"], | |
| system=best["system"], price_usd=best["price_usd"], | |
| price_kind=best["price_kind"], evidence=best["evidence"], | |
| confidence=best["confidence"], source_url=best["source_url"], | |
| note=best["note"], item_id=best["item_id"], | |
| price_event_id=best["price_event_id"], | |
| bandwidth_gbps=best["bandwidth_gbps"], | |
| bandwidth_source_url=best["bandwidth_source_url"], | |
| bandwidth_note=best["bandwidth_note"], | |
| price_per_memory_gb=best["price_per_memory_gb"], | |
| )) | |
| # Derive the monotonic dominant frontier for every price band. | |
| dominant_frontier: list[dict[str, Any]] = [] | |
| for band_key, band_label, _, _ in BANDS: | |
| incumbent: dict[str, Any] | None = None | |
| incumbent_year = "" | |
| for year in YEARS: | |
| candidates = [ | |
| r for r in catalog | |
| if r["price_band"] == band_key | |
| and r["start_year"] <= year <= r["end_year"] | |
| ] | |
| annual_best = ( | |
| max(candidates, key=lambda r: (r["memory_gb"], EVIDENCE_RANK[r["evidence"]])) | |
| if candidates else None | |
| ) | |
| if annual_best is not None and ( | |
| incumbent is None or annual_best["memory_gb"] > incumbent["memory_gb"] | |
| ): | |
| incumbent = annual_best | |
| incumbent_year = year | |
| if incumbent is None: | |
| dominant_frontier.append(dict( | |
| year=year, price_band=band_key, price_band_label=band_label, | |
| memory_gb="", memory_type="", symbol="", producer="", system="", | |
| price_usd="", price_kind="", evidence="", confidence="", | |
| source_url="", note="", item_id="", price_event_id="", | |
| bandwidth_gbps="", bandwidth_source_url="", bandwidth_note="", | |
| price_per_memory_gb="", frontier_set_year="", carried_forward="" | |
| )) | |
| continue | |
| dominant_frontier.append(dict( | |
| year=year, price_band=band_key, price_band_label=band_label, | |
| memory_gb=incumbent["memory_gb"], | |
| memory_type=incumbent["memory_type"], | |
| symbol=incumbent["symbol"], | |
| producer=incumbent["producer"], | |
| system=incumbent["system"], | |
| price_usd=incumbent["price_usd"], | |
| price_kind=incumbent["price_kind"], | |
| evidence=incumbent["evidence"], | |
| confidence=incumbent["confidence"], | |
| source_url=incumbent["source_url"], | |
| note=incumbent["note"], | |
| item_id=incumbent["item_id"], | |
| price_event_id=incumbent["price_event_id"], | |
| bandwidth_gbps=incumbent["bandwidth_gbps"], | |
| bandwidth_source_url=incumbent["bandwidth_source_url"], | |
| bandwidth_note=incumbent["bandwidth_note"], | |
| price_per_memory_gb=incumbent["price_per_memory_gb"], | |
| frontier_set_year=incumbent_year, | |
| carried_forward=year != incumbent_year, | |
| )) | |
| # Record releases that meet the carried frontier even when they do not raise it. | |
| dominant_by_band_year = { | |
| (r["price_band"], r["year"]): r | |
| for r in dominant_frontier | |
| } | |
| dominant_events: list[dict[str, Any]] = [] | |
| seen_event_keys: set[tuple[Any, ...]] = set() | |
| for band_key, band_label, _, _ in BANDS: | |
| final_row = next( | |
| r for r in reversed(dominant_frontier) | |
| if r["price_band"] == band_key and r["memory_gb"] != "" | |
| ) | |
| final_held_from = ( | |
| final_row["frontier_set_year"] | |
| if final_row["carried_forward"] is True else "" | |
| ) | |
| for year in YEARS: | |
| frontier_row = dominant_by_band_year[(band_key, year)] | |
| if frontier_row["memory_gb"] == "": | |
| continue | |
| frontier_value = frontier_row["memory_gb"] | |
| active_matches = [ | |
| r for r in catalog | |
| if r["price_band"] == band_key | |
| and r["start_year"] <= year <= r["end_year"] | |
| and r["memory_gb"] == frontier_value | |
| ] | |
| events = [ | |
| r for r in active_matches | |
| if r["start_year"] == year | |
| or ( | |
| year == frontier_row["frontier_set_year"] | |
| and r["system"] == frontier_row["system"] | |
| and r["producer"] == frontier_row["producer"] | |
| ) | |
| ] | |
| events.sort( | |
| key=lambda r: ( | |
| r["producer"], | |
| -EVIDENCE_RANK[r["evidence"]], | |
| r["price_usd"], | |
| r["system"], | |
| ) | |
| ) | |
| for event in events: | |
| key = ( | |
| year, band_key, event["producer"], event["system"], | |
| event["price_usd"], event["memory_gb"], event["evidence"], | |
| ) | |
| if key in seen_event_keys: | |
| continue | |
| seen_event_keys.add(key) | |
| is_frontier_setter = ( | |
| year == frontier_row["frontier_set_year"] | |
| and event["producer"] == frontier_row["producer"] | |
| and event["system"] == frontier_row["system"] | |
| and event["price_usd"] == frontier_row["price_usd"] | |
| ) | |
| dominant_events.append(dict( | |
| year=year, | |
| price_band=band_key, | |
| price_band_label=band_label, | |
| memory_gb=event["memory_gb"], | |
| memory_type=event["memory_type"], | |
| symbol=event["symbol"], | |
| producer=event["producer"], | |
| system=event["system"], | |
| price_usd=event["price_usd"], | |
| evidence=event["evidence"], | |
| confidence=event["confidence"], | |
| source_url=event["source_url"], | |
| note=event["note"], | |
| item_id=event["item_id"], | |
| price_event_id=event["price_event_id"], | |
| bandwidth_gbps=event["bandwidth_gbps"], | |
| bandwidth_source_url=event["bandwidth_source_url"], | |
| bandwidth_note=event["bandwidth_note"], | |
| price_per_memory_gb=event["price_per_memory_gb"], | |
| price_kind=event["price_kind"], | |
| event_kind=( | |
| "frontier_setter" | |
| if is_frontier_setter | |
| else "frontier_match_price_update" | |
| if event["price_kind"] == "updated_msrp" | |
| else "frontier_match_release" | |
| ), | |
| held_through=( | |
| final_row["year"] | |
| if is_frontier_setter and final_held_from == year | |
| else "" | |
| ), | |
| )) | |
| # Write source catalog CSV. | |
| catalog_fields = [ | |
| "item_id", "price_event_id", "producer", "system", "start_year", | |
| "end_year", "price_usd", "price_band", "price_kind", "memory_gb", | |
| "memory_type", "symbol", "bandwidth_gbps", "bandwidth_source_url", | |
| "bandwidth_note", "price_per_memory_gb", "evidence", "confidence", | |
| "source_url", "note" | |
| ] | |
| catalog_path = DATA_DIR / "verified_system_catalog.csv" | |
| with catalog_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=catalog_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(catalog) | |
| # Write extracted component/memory-price USD/GB rows. | |
| memory_price_fields = [ | |
| "pricing_id", "item_id", "producer", "item_name", "component_label", | |
| "pricing_method", "price_scope", "price_year", "priced_memory_gb", | |
| "memory_price_usd", "memory_price_per_gb", "evidence", "confidence", | |
| "source_url", "note", | |
| ] | |
| memory_price_path = DATA_DIR / "memory_price_per_gb.csv" | |
| with memory_price_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=memory_price_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(memory_price_rows) | |
| # Write derived annual frontier CSV. | |
| frontier_fields = [ | |
| "year", "price_band", "price_band_label", "producer", "memory_gb", | |
| "memory_type", "symbol", "system", "price_usd", "price_kind", | |
| "bandwidth_gbps", "bandwidth_source_url", "bandwidth_note", | |
| "price_per_memory_gb", "evidence", "confidence", "source_url", "note", | |
| "item_id", "price_event_id" | |
| ] | |
| frontier_path = DATA_DIR / "annual_frontier_2020_2026.csv" | |
| with frontier_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=frontier_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(frontier) | |
| # Write the monotonic dominant frontier CSV. | |
| dominant_fields = [ | |
| "year", "price_band", "price_band_label", "memory_gb", "memory_type", | |
| "symbol", "producer", "system", "price_usd", "price_kind", | |
| "bandwidth_gbps", "bandwidth_source_url", "bandwidth_note", | |
| "price_per_memory_gb", "evidence", "confidence", "source_url", "note", | |
| "item_id", "price_event_id", | |
| "frontier_set_year", "carried_forward" | |
| ] | |
| dominant_path = DATA_DIR / "dominant_frontier_2020_2026.csv" | |
| with dominant_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=dominant_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(dominant_frontier) | |
| # Write the release/event labels used by the monotonic charts. | |
| event_fields = [ | |
| "year", "price_band", "price_band_label", "memory_gb", "memory_type", | |
| "symbol", "producer", "system", "price_usd", "price_kind", | |
| "bandwidth_gbps", "bandwidth_source_url", "bandwidth_note", | |
| "price_per_memory_gb", "evidence", "confidence", "source_url", "note", | |
| "item_id", "price_event_id", | |
| "event_kind", "held_through" | |
| ] | |
| events_path = DATA_DIR / "dominant_frontier_events_2020_2026.csv" | |
| with events_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=event_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(dominant_events) | |
| # A compact matrix for quick inspection. | |
| matrix_path = DATA_DIR / "frontier_matrix.csv" | |
| with matrix_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.writer(f, lineterminator="\n") | |
| writer.writerow(["price_band", "producer"] + YEARS) | |
| for band_key, band_label, _, _ in BANDS: | |
| for producer in PRODUCERS: | |
| vals = [] | |
| for year in YEARS: | |
| row = next( | |
| r for r in frontier | |
| if r["price_band"] == band_key | |
| and r["producer"] == producer | |
| and r["year"] == year | |
| ) | |
| if row["memory_gb"] == "": | |
| vals.append("") | |
| else: | |
| suffix = row["symbol"] | |
| if row["evidence"] == "bom_inferred": | |
| suffix += "*" | |
| vals.append(f'{row["memory_gb"]}{suffix}') | |
| writer.writerow([band_label, producer] + vals) | |
| def save_chart(fig: Any, path: Path) -> tuple[Path, Path]: | |
| fig.savefig(path, dpi=190, bbox_inches="tight") | |
| svg_path = path.with_suffix(".svg") | |
| fig.savefig(svg_path, bbox_inches="tight", metadata={"Date": None}) | |
| svg_text = svg_path.read_text(encoding="utf-8") | |
| svg_path.write_text( | |
| "\n".join(line.rstrip() for line in svg_text.splitlines()) + "\n", | |
| encoding="utf-8", | |
| ) | |
| return path, svg_path | |
| # Plot each price band as its own figure. | |
| chart_paths: list[Path] = [] | |
| chart_svg_paths: list[Path] = [] | |
| POINT_LABEL_OFFSET = 9 | |
| MONOTONIC_LABEL_OFFSET = 28 | |
| for index, (band_key, band_label, _, _) in enumerate(BANDS, start=1): | |
| fig, ax = plt.subplots(figsize=(11.5, 7.2)) | |
| max_val = 0 | |
| for producer in PRODUCERS: | |
| rows = [ | |
| r for r in frontier | |
| if r["price_band"] == band_key and r["producer"] == producer | |
| ] | |
| values = [ | |
| float(r["memory_gb"]) if r["memory_gb"] != "" else math.nan | |
| for r in rows | |
| ] | |
| finite_values = [v for v in values if not math.isnan(v)] | |
| if finite_values: | |
| max_val = max(max_val, max(finite_values)) | |
| line, = ax.plot(YEARS, values, linewidth=2.2, label=producer) | |
| line_color = line.get_color() | |
| else: | |
| line, = ax.plot([], [], linewidth=2.2, label=producer) | |
| line_color = line.get_color() | |
| for r, value in zip(rows, values): | |
| if math.isnan(value): | |
| continue | |
| marker = {"U": "o", "V": "s", "Σ": "^"}[r["symbol"]] | |
| if r["evidence"] == "bom_inferred": | |
| ax.scatter( | |
| r["year"], value, marker=marker, s=70, | |
| facecolors="none", edgecolors=line_color, linewidths=1.7, zorder=4 | |
| ) | |
| estimate_mark = "*" | |
| else: | |
| ax.scatter( | |
| r["year"], value, marker=marker, s=70, | |
| c=[line_color], zorder=4 | |
| ) | |
| estimate_mark = "" | |
| ax.annotate( | |
| f'{int(value)}{r["symbol"]}{estimate_mark}', | |
| (r["year"], value), | |
| xytext=(0, POINT_LABEL_OFFSET), textcoords="offset points", | |
| ha="center", va="bottom", fontsize=8.5 | |
| ) | |
| ax.set_title( | |
| f"Maximum accelerator-accessible memory in one machine — {band_label}", | |
| fontsize=15, pad=18 | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel("Memory capacity (GB)") | |
| ax.set_xticks(YEARS) | |
| ax.set_xlim(min(YEARS) - 0.25, max(YEARS) + 0.25) | |
| upper = max(64, max_val * 1.25) | |
| ax.set_ylim(0, upper) | |
| ax.grid(axis="y", alpha=0.25) | |
| ax.legend(title="Chip/platform producer", loc="upper left") | |
| footer = ( | |
| "U = unified/coherent memory V = one discrete GPU's VRAM " | |
| "Σ = aggregate VRAM across GPUs in one chassis\n" | |
| "Filled marker = quoted/reconstructed complete configuration " | |
| "Open marker + * = price band inferred from documented component BOM " | |
| "Blank = no qualifying configuration verified" | |
| ) | |
| if band_key == "5000_10000": | |
| footer += "\nApple 512U is the real 2025 M3 Ultra launch option; it was removed before the June 2026 current-market snapshot." | |
| fig.text(0.5, 0.015, footer, ha="center", va="bottom", fontsize=8.2) | |
| fig.tight_layout(rect=(0.03, 0.09, 0.98, 0.96)) | |
| chart_path = CHARTS_DIR / f"{index:02d}_{band_key}.png" | |
| chart_path, svg_path = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| chart_paths.append(chart_path) | |
| chart_svg_paths.append(svg_path) | |
| def metric_value(row: dict[str, Any], key: str) -> float: | |
| if row[key] == "": | |
| return math.nan | |
| return float(row[key]) | |
| def metric_marker(row: dict[str, Any]) -> str: | |
| return {"U": "o", "V": "s", "Σ": "^"}[row["symbol"]] | |
| def plot_metric_family( | |
| *, | |
| metric_key: str, | |
| output_dir: Path, | |
| filename_suffix: str, | |
| title_prefix: str, | |
| ylabel: str, | |
| label_formatter: Any, | |
| footer: str, | |
| legend_loc: str, | |
| ) -> tuple[list[Path], list[Path]]: | |
| paths: list[Path] = [] | |
| svg_paths: list[Path] = [] | |
| for index, (band_key, band_label, _, _) in enumerate(BANDS, start=1): | |
| fig, ax = plt.subplots(figsize=(11.5, 7.2)) | |
| max_val = 0.0 | |
| for producer in PRODUCERS: | |
| rows = [ | |
| r for r in frontier | |
| if r["price_band"] == band_key and r["producer"] == producer | |
| ] | |
| values = [metric_value(r, metric_key) for r in rows] | |
| finite_values = [v for v in values if not math.isnan(v)] | |
| if finite_values: | |
| max_val = max(max_val, max(finite_values)) | |
| line, = ax.plot(YEARS, values, linewidth=2.2, label=producer) | |
| line_color = line.get_color() | |
| else: | |
| line, = ax.plot([], [], linewidth=2.2, label=producer) | |
| line_color = line.get_color() | |
| for r, value in zip(rows, values): | |
| if math.isnan(value): | |
| continue | |
| if r["evidence"] == "bom_inferred": | |
| ax.scatter( | |
| r["year"], value, marker=metric_marker(r), s=70, | |
| facecolors="none", edgecolors=line_color, | |
| linewidths=1.7, zorder=4 | |
| ) | |
| else: | |
| ax.scatter( | |
| r["year"], value, marker=metric_marker(r), s=70, | |
| c=[line_color], zorder=4 | |
| ) | |
| ax.annotate( | |
| label_formatter(value), | |
| (r["year"], value), | |
| xytext=(0, POINT_LABEL_OFFSET), textcoords="offset points", | |
| ha="center", va="bottom", fontsize=8.0 | |
| ) | |
| safe_band_label = band_label.replace("$", r"\$") | |
| ax.set_title( | |
| f"{title_prefix} — {safe_band_label}", | |
| fontsize=15, pad=18 | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel(ylabel) | |
| ax.set_xticks(YEARS) | |
| ax.set_xlim(min(YEARS) - 0.25, max(YEARS) + 0.25) | |
| upper = max(1.0, max_val * 1.25) | |
| ax.set_ylim(0, upper) | |
| ax.grid(axis="y", alpha=0.25) | |
| ax.legend(title="Chip/platform producer", loc=legend_loc) | |
| fig.text(0.5, 0.015, footer, ha="center", va="bottom", fontsize=8.2) | |
| fig.tight_layout(rect=(0.03, 0.09, 0.98, 0.96)) | |
| chart_path = output_dir / f"{index:02d}_{band_key}_{filename_suffix}.png" | |
| chart_path, svg_path = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| paths.append(chart_path) | |
| svg_paths.append(svg_path) | |
| return paths, svg_paths | |
| price_per_memory_chart_paths, price_per_memory_chart_svg_paths = plot_metric_family( | |
| metric_key="price_per_memory_gb", | |
| output_dir=PRICE_PER_MEMORY_CHARTS_DIR, | |
| filename_suffix="price_per_memory", | |
| title_prefix="Complete-system price per GB of accelerator-accessible memory", | |
| ylabel="USD per GB", | |
| label_formatter=lambda value: f"${value:,.0f}/GB", | |
| footer=( | |
| "USD/GB = complete-system price divided by accelerator-accessible memory " | |
| "for the selected annual max-memory configuration." | |
| ), | |
| legend_loc="upper right", | |
| ) | |
| bandwidth_chart_paths, bandwidth_chart_svg_paths = plot_metric_family( | |
| metric_key="bandwidth_gbps", | |
| output_dir=BANDWIDTH_CHARTS_DIR, | |
| filename_suffix="bandwidth", | |
| title_prefix="Peak memory bandwidth for the selected memory frontier system", | |
| ylabel="Peak memory bandwidth (GB/s)", | |
| label_formatter=lambda value: f"{value:,.0f}", | |
| footer=( | |
| "Bandwidth comes from item JSON source fields. Σ uses aggregate installed-GPU " | |
| "bandwidth; tiered-memory caveats are recorded in item JSON and CSV notes." | |
| ), | |
| legend_loc="upper left", | |
| ) | |
| def value_label(row: dict[str, Any]) -> str: | |
| suffix = row["symbol"] | |
| if row["evidence"] == "bom_inferred": | |
| suffix += "*" | |
| return f'{int(row["memory_gb"])}{suffix}' | |
| def price_label(row: dict[str, Any]) -> str: | |
| return f'${int(row["price_usd"]):,}' | |
| def device_label(row: dict[str, Any]) -> str: | |
| device = textwrap.shorten( | |
| f'{row["producer"]}: {row["system"]}', | |
| width=44, | |
| placeholder="...", | |
| ) | |
| lines = [f"{value_label(row)} {device}", price_label(row)] | |
| if row.get("held_through"): | |
| lines.append(f'held through {row["held_through"]}') | |
| return "\n".join(lines) | |
| def best_metric_label( | |
| row: dict[str, Any], | |
| *, | |
| metric_key: str, | |
| metric_formatter: Any, | |
| ) -> str: | |
| device = textwrap.shorten( | |
| f'{row["producer"]}: {row["system"]}', | |
| width=44, | |
| placeholder="...", | |
| ) | |
| if metric_key == "price_per_memory_gb": | |
| lines = [ | |
| device, | |
| f'price per GB: {metric_formatter(float(row[metric_key]))}', | |
| ] | |
| lines.append(f'{value_label(row)}, {price_label(row)} total') | |
| else: | |
| lines = [f'{metric_formatter(float(row[metric_key]))} {device}'] | |
| lines.append(f'{value_label(row)}, {price_label(row)}') | |
| if row.get("held_through"): | |
| lines.append(f'held through {row["held_through"]}') | |
| return "\n".join(lines) | |
| def choose_metric_candidate( | |
| candidates: list[dict[str, Any]], | |
| *, | |
| metric_key: str, | |
| lower_is_better: bool, | |
| ) -> dict[str, Any] | None: | |
| valid = [row for row in candidates if row.get(metric_key) != ""] | |
| if not valid: | |
| return None | |
| if lower_is_better: | |
| return min( | |
| valid, | |
| key=lambda row: ( | |
| float(row[metric_key]), | |
| -int(row["memory_gb"]), | |
| -EVIDENCE_RANK[row["evidence"]], | |
| int(row["price_usd"]), | |
| row["system"], | |
| ), | |
| ) | |
| return max( | |
| valid, | |
| key=lambda row: ( | |
| float(row[metric_key]), | |
| int(row["memory_gb"]), | |
| EVIDENCE_RANK[row["evidence"]], | |
| -int(row["price_usd"]), | |
| row["system"], | |
| ), | |
| ) | |
| def metric_is_better( | |
| candidate: dict[str, Any], | |
| incumbent: dict[str, Any] | None, | |
| *, | |
| metric_key: str, | |
| lower_is_better: bool, | |
| ) -> bool: | |
| if incumbent is None: | |
| return True | |
| candidate_value = float(candidate[metric_key]) | |
| incumbent_value = float(incumbent[metric_key]) | |
| if lower_is_better: | |
| return candidate_value < incumbent_value | |
| return candidate_value > incumbent_value | |
| def empty_metric_row( | |
| *, | |
| year: int, | |
| band_key: str, | |
| band_label: str, | |
| metric_name: str, | |
| ) -> dict[str, Any]: | |
| return dict( | |
| year=year, price_band=band_key, price_band_label=band_label, | |
| metric_name=metric_name, metric_value="", memory_gb="", memory_type="", | |
| symbol="", producer="", system="", price_usd="", price_kind="", | |
| bandwidth_gbps="", bandwidth_source_url="", bandwidth_note="", | |
| price_per_memory_gb="", evidence="", confidence="", source_url="", | |
| note="", item_id="", price_event_id="", frontier_set_year="", | |
| carried_forward="", | |
| ) | |
| def metric_frontier_row( | |
| *, | |
| year: int, | |
| band_key: str, | |
| band_label: str, | |
| metric_name: str, | |
| metric_key: str, | |
| source: dict[str, Any], | |
| frontier_set_year: int, | |
| ) -> dict[str, Any]: | |
| return dict( | |
| year=year, | |
| price_band=band_key, | |
| price_band_label=band_label, | |
| metric_name=metric_name, | |
| metric_value=source[metric_key], | |
| memory_gb=source["memory_gb"], | |
| memory_type=source["memory_type"], | |
| symbol=source["symbol"], | |
| producer=source["producer"], | |
| system=source["system"], | |
| price_usd=source["price_usd"], | |
| price_kind=source["price_kind"], | |
| bandwidth_gbps=source["bandwidth_gbps"], | |
| bandwidth_source_url=source["bandwidth_source_url"], | |
| bandwidth_note=source["bandwidth_note"], | |
| price_per_memory_gb=source["price_per_memory_gb"], | |
| evidence=source["evidence"], | |
| confidence=source["confidence"], | |
| source_url=source["source_url"], | |
| note=source["note"], | |
| item_id=source["item_id"], | |
| price_event_id=source["price_event_id"], | |
| frontier_set_year=frontier_set_year, | |
| carried_forward=year != frontier_set_year, | |
| ) | |
| def derive_best_metric_frontier( | |
| *, | |
| metric_key: str, | |
| metric_name: str, | |
| lower_is_better: bool, | |
| ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: | |
| rows: list[dict[str, Any]] = [] | |
| events: list[dict[str, Any]] = [] | |
| seen_event_keys: set[tuple[Any, ...]] = set() | |
| for band_key, band_label, _, _ in BANDS: | |
| incumbent: dict[str, Any] | None = None | |
| incumbent_year = "" | |
| band_rows: list[dict[str, Any]] = [] | |
| for year in YEARS: | |
| candidates = [ | |
| row for row in catalog | |
| if row["price_band"] == band_key | |
| and row["start_year"] <= year <= row["end_year"] | |
| ] | |
| annual_best = choose_metric_candidate( | |
| candidates, | |
| metric_key=metric_key, | |
| lower_is_better=lower_is_better, | |
| ) | |
| if annual_best is not None and metric_is_better( | |
| annual_best, | |
| incumbent, | |
| metric_key=metric_key, | |
| lower_is_better=lower_is_better, | |
| ): | |
| incumbent = annual_best | |
| incumbent_year = year | |
| if incumbent is None: | |
| row = empty_metric_row( | |
| year=year, | |
| band_key=band_key, | |
| band_label=band_label, | |
| metric_name=metric_name, | |
| ) | |
| else: | |
| row = metric_frontier_row( | |
| year=year, | |
| band_key=band_key, | |
| band_label=band_label, | |
| metric_name=metric_name, | |
| metric_key=metric_key, | |
| source=incumbent, | |
| frontier_set_year=int(incumbent_year), | |
| ) | |
| rows.append(row) | |
| band_rows.append(row) | |
| final_row = next( | |
| (row for row in reversed(band_rows) if row["metric_value"] != ""), | |
| None, | |
| ) | |
| if final_row is None: | |
| continue | |
| final_held_from = ( | |
| final_row["frontier_set_year"] | |
| if final_row["carried_forward"] is True else "" | |
| ) | |
| for year in YEARS: | |
| frontier_row = next(row for row in band_rows if row["year"] == year) | |
| if frontier_row["metric_value"] == "": | |
| continue | |
| frontier_value = float(frontier_row["metric_value"]) | |
| matching_events = [ | |
| row for row in catalog | |
| if row["price_band"] == band_key | |
| and row["start_year"] <= year <= row["end_year"] | |
| and row.get(metric_key) != "" | |
| and math.isclose(float(row[metric_key]), frontier_value) | |
| and year == frontier_row["frontier_set_year"] | |
| and row["system"] == frontier_row["system"] | |
| and row["producer"] == frontier_row["producer"] | |
| and row["price_usd"] == frontier_row["price_usd"] | |
| ] | |
| matching_events.sort( | |
| key=lambda row: ( | |
| row["producer"], | |
| -EVIDENCE_RANK[row["evidence"]], | |
| row["price_usd"], | |
| row["system"], | |
| ) | |
| ) | |
| for event in matching_events: | |
| key = ( | |
| metric_name, year, band_key, event["producer"], | |
| event["system"], event["price_usd"], event[metric_key], | |
| ) | |
| if key in seen_event_keys: | |
| continue | |
| seen_event_keys.add(key) | |
| is_frontier_setter = ( | |
| year == frontier_row["frontier_set_year"] | |
| and event["producer"] == frontier_row["producer"] | |
| and event["system"] == frontier_row["system"] | |
| and event["price_usd"] == frontier_row["price_usd"] | |
| ) | |
| events.append(dict( | |
| **metric_frontier_row( | |
| year=year, | |
| band_key=band_key, | |
| band_label=band_label, | |
| metric_name=metric_name, | |
| metric_key=metric_key, | |
| source=event, | |
| frontier_set_year=int(frontier_row["frontier_set_year"]), | |
| ), | |
| event_kind=( | |
| "frontier_setter" | |
| if is_frontier_setter | |
| else "frontier_match_price_update" | |
| if event["price_kind"] == "updated_msrp" | |
| else "frontier_match_release" | |
| ), | |
| held_through=( | |
| final_row["year"] | |
| if is_frontier_setter and final_held_from == year | |
| else "" | |
| ), | |
| )) | |
| return rows, events | |
| def write_metric_csv( | |
| rows: list[dict[str, Any]], | |
| events: list[dict[str, Any]], | |
| *, | |
| frontier_path: Path, | |
| events_path: Path, | |
| ) -> None: | |
| fields = [ | |
| "year", "price_band", "price_band_label", "metric_name", | |
| "metric_value", "memory_gb", "memory_type", "symbol", "producer", | |
| "system", "price_usd", "price_kind", "bandwidth_gbps", | |
| "bandwidth_source_url", "bandwidth_note", "price_per_memory_gb", | |
| "evidence", "confidence", "source_url", "note", "item_id", | |
| "price_event_id", "frontier_set_year", "carried_forward", | |
| ] | |
| with frontier_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(rows) | |
| event_fields = [*fields, "event_kind", "held_through"] | |
| with events_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=event_fields, lineterminator="\n") | |
| writer.writeheader() | |
| writer.writerows(events) | |
| def plot_best_metric_frontier( | |
| rows: list[dict[str, Any]], | |
| events: list[dict[str, Any]], | |
| *, | |
| output_dir: Path, | |
| filename_suffix: str, | |
| title_prefix: str, | |
| ylabel: str, | |
| metric_key: str, | |
| metric_formatter: Any, | |
| footer: str, | |
| ) -> tuple[list[Path], list[Path]]: | |
| paths: list[Path] = [] | |
| svg_paths: list[Path] = [] | |
| for index, (band_key, band_label, _, _) in enumerate(BANDS, start=1): | |
| band_rows = [row for row in rows if row["price_band"] == band_key] | |
| values = [ | |
| float(row["metric_value"]) if row["metric_value"] != "" else math.nan | |
| for row in band_rows | |
| ] | |
| finite_values = [value for value in values if not math.isnan(value)] | |
| if not finite_values: | |
| continue | |
| fig, ax = plt.subplots(figsize=(13.2, 8.0)) | |
| max_val = max(finite_values) | |
| ax.step(YEARS, values, where="post", linewidth=2.6, color="#111827") | |
| events_by_point: dict[tuple[int, float], list[dict[str, Any]]] = {} | |
| for event in events: | |
| if event["price_band"] != band_key: | |
| continue | |
| point = (int(event["year"]), round(float(event["metric_value"]), 4)) | |
| events_by_point.setdefault(point, []).append(event) | |
| for row, value in zip(band_rows, values): | |
| if math.isnan(value): | |
| continue | |
| marker = {"U": "o", "V": "s", "Σ": "^"}[row["symbol"]] | |
| inferred = row["evidence"] == "bom_inferred" | |
| if inferred: | |
| ax.scatter( | |
| int(row["year"]), value, marker=marker, s=78, | |
| facecolors="white", edgecolors="#111827", | |
| linewidths=1.7, zorder=4 | |
| ) | |
| else: | |
| ax.scatter( | |
| int(row["year"]), value, marker=marker, s=78, | |
| c=["#111827"], zorder=4 | |
| ) | |
| year = int(row["year"]) | |
| point_events = events_by_point.get((year, round(value, 4)), []) | |
| if not point_events: | |
| continue | |
| label = "\n\n".join( | |
| best_metric_label( | |
| event, | |
| metric_key=metric_key, | |
| metric_formatter=metric_formatter, | |
| ) | |
| for event in point_events | |
| ) | |
| ax.annotate( | |
| label, | |
| (year, value), | |
| xytext=(0, MONOTONIC_LABEL_OFFSET), | |
| textcoords="offset points", | |
| ha="center", va="bottom", fontsize=6.6, | |
| arrowprops=dict(arrowstyle="-", color="#6b7280", lw=0.8), | |
| bbox=dict(boxstyle="round,pad=0.22", fc="white", ec="none", alpha=0.82), | |
| annotation_clip=False, | |
| ) | |
| safe_band_label = band_label.replace("$", r"\$") | |
| ax.set_title(f"{title_prefix} — {safe_band_label}", fontsize=15, pad=18) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel(ylabel) | |
| ax.set_xticks(YEARS) | |
| ax.set_xlim(min(YEARS) - 0.9, max(YEARS) + 0.9) | |
| ax.set_ylim(0, max(1.0, max_val * 1.55)) | |
| ax.grid(axis="y", alpha=0.25) | |
| fig.text(0.5, 0.015, footer, ha="center", va="bottom", fontsize=8.2) | |
| fig.tight_layout(rect=(0.03, 0.10, 0.98, 0.95)) | |
| chart_path = output_dir / f"{index:02d}_{band_key}_{filename_suffix}.png" | |
| chart_path, svg_path = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| paths.append(chart_path) | |
| svg_paths.append(svg_path) | |
| return paths, svg_paths | |
| # Plot each price band's best-so-far frontier as a monotonic line. | |
| monotonic_chart_paths: list[Path] = [] | |
| monotonic_chart_svg_paths: list[Path] = [] | |
| for index, (band_key, band_label, _, _) in enumerate(BANDS, start=1): | |
| rows = [ | |
| r for r in dominant_frontier | |
| if r["price_band"] == band_key | |
| ] | |
| values = [ | |
| float(r["memory_gb"]) if r["memory_gb"] != "" else math.nan | |
| for r in rows | |
| ] | |
| finite_values = [v for v in values if not math.isnan(v)] | |
| if not finite_values: | |
| continue | |
| fig, ax = plt.subplots(figsize=(13.2, 8.0)) | |
| max_val = max(finite_values) | |
| ax.step(YEARS, values, where="post", linewidth=2.6, color="#111827") | |
| events_by_point: dict[tuple[int, int], list[dict[str, Any]]] = {} | |
| for event in dominant_events: | |
| if event["price_band"] != band_key: | |
| continue | |
| point = (int(event["year"]), int(event["memory_gb"])) | |
| events_by_point.setdefault(point, []).append(event) | |
| for r, value in zip(rows, values): | |
| if math.isnan(value): | |
| continue | |
| marker = {"U": "o", "V": "s", "Σ": "^"}[r["symbol"]] | |
| inferred = r["evidence"] == "bom_inferred" | |
| if inferred: | |
| ax.scatter( | |
| int(r["year"]), value, marker=marker, s=78, | |
| facecolors="white", edgecolors="#111827", | |
| linewidths=1.7, zorder=4 | |
| ) | |
| else: | |
| ax.scatter( | |
| int(r["year"]), value, marker=marker, s=78, | |
| c=["#111827"], zorder=4 | |
| ) | |
| year = int(r["year"]) | |
| point_events = events_by_point.get((year, int(value)), []) | |
| if not point_events: | |
| continue | |
| label = "\n\n".join(device_label(event) for event in point_events) | |
| ax.annotate( | |
| label, | |
| (year, value), | |
| xytext=(0, MONOTONIC_LABEL_OFFSET), | |
| textcoords="offset points", | |
| ha="center", va="bottom", fontsize=6.6, | |
| arrowprops=dict(arrowstyle="-", color="#6b7280", lw=0.8), | |
| bbox=dict(boxstyle="round,pad=0.22", fc="white", ec="none", alpha=0.82), | |
| annotation_clip=False, | |
| ) | |
| safe_band_label = band_label.replace("$", r"\$") | |
| ax.set_title( | |
| f"Best-so-far accelerator-accessible memory in one machine — {safe_band_label}", | |
| fontsize=15, pad=18 | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel("Memory capacity (GB)") | |
| ax.set_xticks(YEARS) | |
| ax.set_xlim(min(YEARS) - 0.9, max(YEARS) + 0.9) | |
| ax.set_ylim(0, max(64, max_val * 1.55)) | |
| ax.grid(axis="y", alpha=0.25) | |
| footer = ( | |
| "Monotonic frontier: if the current market drops, the line carries the previous maximum forward.\n" | |
| "Labels name releases or price updates that set or match the carried frontier; prices are complete-system USD." | |
| ) | |
| fig.text(0.5, 0.015, footer, ha="center", va="bottom", fontsize=8.2) | |
| fig.tight_layout(rect=(0.03, 0.10, 0.98, 0.95)) | |
| chart_path = MONOTONIC_CHARTS_DIR / f"{index:02d}_{band_key}_monotonic.png" | |
| chart_path, svg_path = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| monotonic_chart_paths.append(chart_path) | |
| monotonic_chart_svg_paths.append(svg_path) | |
| def plot_consumer_best_memory_chart() -> tuple[Path, Path]: | |
| consumer_band_keys = ["under_3000", "3000_5000", "5000_10000"] | |
| consumer_band_labels = { | |
| "under_3000": "Under 3k", | |
| "3000_5000": "3k-5k", | |
| "5000_10000": "5k-10k", | |
| } | |
| fig, ax = plt.subplots(figsize=(13.2, 8.0)) | |
| max_val = 0.0 | |
| for band_key in consumer_band_keys: | |
| rows = [ | |
| r for r in dominant_frontier | |
| if r["price_band"] == band_key | |
| ] | |
| values = [ | |
| float(r["memory_gb"]) if r["memory_gb"] != "" else math.nan | |
| for r in rows | |
| ] | |
| finite_values = [value for value in values if not math.isnan(value)] | |
| if finite_values: | |
| max_val = max(max_val, max(finite_values)) | |
| ax.step( | |
| YEARS, | |
| values, | |
| where="post", | |
| linewidth=2.6, | |
| marker="o", | |
| markersize=5.2, | |
| label=consumer_band_labels[band_key], | |
| ) | |
| ax.set_title( | |
| "Best-so-far accelerator-accessible memory — consumer price bands", | |
| fontsize=15, | |
| pad=18, | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel("Memory capacity (GB)") | |
| ax.set_xticks(YEARS) | |
| ax.set_xlim(min(YEARS) - 0.45, max(YEARS) + 0.45) | |
| ax.set_ylim(0, max(64, max_val * 1.18)) | |
| ax.grid(axis="y", alpha=0.25) | |
| ax.legend(title="Complete-machine price band", loc="upper left") | |
| fig.text( | |
| 0.5, | |
| 0.015, | |
| "Same axes for under 3k, 3k-5k, and 5k-10k. Lines carry the prior best forward if the current market drops.", | |
| ha="center", | |
| va="bottom", | |
| fontsize=8.2, | |
| ) | |
| fig.tight_layout(rect=(0.03, 0.08, 0.98, 0.95)) | |
| chart_path = MONOTONIC_CHARTS_DIR / "00_consumer_bands_monotonic.png" | |
| output_paths = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| return output_paths | |
| consumer_monotonic_chart_path, consumer_monotonic_chart_svg_path = ( | |
| plot_consumer_best_memory_chart() | |
| ) | |
| def linear_fit(xs: list[int], ys: list[float]) -> tuple[float, float]: | |
| x_mean = sum(xs) / len(xs) | |
| y_mean = sum(ys) / len(ys) | |
| denominator = sum((x - x_mean) ** 2 for x in xs) | |
| if denominator == 0: | |
| raise ValueError("linear fit needs at least two distinct x values") | |
| slope = sum((x - x_mean) * (y - y_mean) for x, y in zip(xs, ys)) / denominator | |
| intercept = y_mean - slope * x_mean | |
| return slope, intercept | |
| def derive_memory_projection_rows( | |
| band_key: str, | |
| band_label: str, | |
| ) -> list[dict[str, Any]]: | |
| historical_rows = [ | |
| row for row in dominant_frontier | |
| if row["price_band"] == band_key and row["memory_gb"] != "" | |
| ] | |
| fit_years = [int(row["year"]) for row in historical_rows] | |
| fit_values = [float(row["memory_gb"]) for row in historical_rows] | |
| slope, intercept = linear_fit(fit_years, fit_values) | |
| base_year = min(fit_years) | |
| base_memory_gb = intercept + slope * base_year | |
| observed_by_year = { | |
| int(row["year"]): float(row["memory_gb"]) | |
| for row in historical_rows | |
| } | |
| rows: list[dict[str, Any]] = [] | |
| for year in range(min(fit_years), 2031): | |
| fitted_value = intercept + slope * year | |
| observed_value = observed_by_year.get(year, "") | |
| rows.append({ | |
| "price_band": band_key, | |
| "price_band_label": band_label, | |
| "year": year, | |
| "observed_best_so_far_memory_gb": ( | |
| int(observed_value) if observed_value != "" else "" | |
| ), | |
| "linear_fit_memory_gb": round(fitted_value, 2), | |
| "is_projection": year > max(fit_years), | |
| "slope_gb_per_year": round(slope, 4), | |
| "fit_base_year": base_year, | |
| "fit_base_memory_gb": round(base_memory_gb, 4), | |
| }) | |
| return rows | |
| consumer_projection_rows: list[dict[str, Any]] = [] | |
| for projection_band_key, projection_band_label, _ in CONSUMER_PROJECTION_BANDS: | |
| consumer_projection_rows.extend( | |
| derive_memory_projection_rows(projection_band_key, projection_band_label) | |
| ) | |
| consumer_projection_path = DATA_DIR / "consumer_memory_linear_projection_2020_2030.csv" | |
| projection_fieldnames = [ | |
| "price_band", "price_band_label", "year", | |
| "observed_best_so_far_memory_gb", "linear_fit_memory_gb", | |
| "is_projection", "slope_gb_per_year", "fit_base_year", | |
| "fit_base_memory_gb", | |
| ] | |
| with consumer_projection_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter( | |
| f, | |
| fieldnames=projection_fieldnames, | |
| lineterminator="\n", | |
| ) | |
| writer.writeheader() | |
| writer.writerows(consumer_projection_rows) | |
| under_3000_projection_rows = [ | |
| { | |
| key: value | |
| for key, value in row.items() | |
| if key not in {"price_band", "price_band_label"} | |
| } | |
| for row in consumer_projection_rows | |
| if row["price_band"] == "under_3000" | |
| ] | |
| projection_path = DATA_DIR / "under_3000_memory_linear_projection_2020_2030.csv" | |
| with projection_path.open("w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter( | |
| f, | |
| fieldnames=[ | |
| "year", "observed_best_so_far_memory_gb", "linear_fit_memory_gb", | |
| "is_projection", "slope_gb_per_year", "fit_base_year", | |
| "fit_base_memory_gb", | |
| ], | |
| lineterminator="\n", | |
| ) | |
| writer.writeheader() | |
| writer.writerows(under_3000_projection_rows) | |
| def plot_consumer_memory_projection( | |
| rows: list[dict[str, Any]], | |
| ) -> tuple[Path, Path]: | |
| years = list(range(2020, 2031)) | |
| fig, ax = plt.subplots(figsize=(13.2, 8.0)) | |
| all_values: list[float] = [] | |
| label_offsets = { | |
| "under_3000": (8, -13), | |
| "3000_5000": (8, 13), | |
| "5000_10000": (8, 0), | |
| } | |
| for band_key, band_label, color in CONSUMER_PROJECTION_BANDS: | |
| band_rows = [row for row in rows if row["price_band"] == band_key] | |
| observed_rows = [ | |
| row for row in band_rows | |
| if row["observed_best_so_far_memory_gb"] != "" | |
| ] | |
| observed_years = [int(row["year"]) for row in observed_rows] | |
| observed_values = [ | |
| float(row["observed_best_so_far_memory_gb"]) | |
| for row in observed_rows | |
| ] | |
| fit_values = [ | |
| float(row["linear_fit_memory_gb"]) | |
| for row in band_rows | |
| ] | |
| slope = float(band_rows[0]["slope_gb_per_year"]) | |
| projected_2030 = next( | |
| float(row["linear_fit_memory_gb"]) | |
| for row in band_rows | |
| if int(row["year"]) == 2030 | |
| ) | |
| all_values.extend(observed_values) | |
| all_values.extend(fit_values) | |
| ax.step( | |
| observed_years, | |
| observed_values, | |
| where="post", | |
| linewidth=2.6, | |
| marker="o", | |
| markersize=5.5, | |
| color=color, | |
| label=f"{band_label} observed best-so-far", | |
| ) | |
| ax.plot( | |
| years, | |
| fit_values, | |
| linewidth=2.1, | |
| linestyle="--", | |
| color=color, | |
| alpha=0.82, | |
| label=f"{band_label} fit: +{slope:,.1f} GB/year", | |
| ) | |
| ax.scatter( | |
| [year for year in years if year > max(observed_years)], | |
| [ | |
| float(row["linear_fit_memory_gb"]) | |
| for row in band_rows | |
| if int(row["year"]) > max(observed_years) | |
| ], | |
| color=color, | |
| s=36, | |
| zorder=4, | |
| ) | |
| ax.annotate( | |
| f"{band_label}: {projected_2030:,.0f} GB", | |
| (2030, projected_2030), | |
| xytext=label_offsets[band_key], | |
| textcoords="offset points", | |
| ha="left", | |
| va="center", | |
| fontsize=8.8, | |
| color=color, | |
| bbox=dict(boxstyle="round,pad=0.20", fc="white", ec="#e5e7eb", alpha=0.86), | |
| ) | |
| ax.axvline(2026, color="#6b7280", linewidth=1.1, linestyle=":") | |
| ax.set_title( | |
| "Consumer-band best-so-far memory with linear projection to 2030", | |
| fontsize=15, | |
| pad=18, | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel("Memory capacity (GB)") | |
| ax.set_xticks(years) | |
| ax.set_xlim(2019.65, 2031.2) | |
| ax.set_ylim(0, max(all_values) * 1.16) | |
| ax.grid(axis="y", alpha=0.25) | |
| ax.legend(loc="upper left", ncols=2, fontsize=8.0, columnspacing=1.1) | |
| fig.text( | |
| 0.5, | |
| 0.015, | |
| "Projection fits one least-squares linear trend per 2020-2026 consumer-band best-so-far memory series; not verified future product data.", | |
| ha="center", | |
| va="bottom", | |
| fontsize=8.2, | |
| ) | |
| fig.tight_layout(rect=(0.03, 0.08, 0.98, 0.95)) | |
| chart_path = PROJECTION_CHARTS_DIR / "consumer_memory_linear_projection.png" | |
| output_paths = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| return output_paths | |
| consumer_projection_chart_path, consumer_projection_chart_svg_path = ( | |
| plot_consumer_memory_projection(consumer_projection_rows) | |
| ) | |
| def plot_under_3000_memory_projection( | |
| rows: list[dict[str, Any]], | |
| ) -> tuple[Path, Path]: | |
| years = [int(row["year"]) for row in rows] | |
| observed_years = [ | |
| int(row["year"]) for row in rows | |
| if row["observed_best_so_far_memory_gb"] != "" | |
| ] | |
| observed_values = [ | |
| float(row["observed_best_so_far_memory_gb"]) for row in rows | |
| if row["observed_best_so_far_memory_gb"] != "" | |
| ] | |
| fit_values = [float(row["linear_fit_memory_gb"]) for row in rows] | |
| projection_rows = [row for row in rows if row["is_projection"] is True] | |
| slope = float(rows[0]["slope_gb_per_year"]) | |
| projected_2030 = next( | |
| float(row["linear_fit_memory_gb"]) | |
| for row in rows | |
| if int(row["year"]) == 2030 | |
| ) | |
| fig, ax = plt.subplots(figsize=(13.2, 8.0)) | |
| ax.step( | |
| observed_years, | |
| observed_values, | |
| where="post", | |
| linewidth=2.8, | |
| marker="o", | |
| markersize=6.0, | |
| color="#2563eb", | |
| label="Observed best-so-far memory", | |
| ) | |
| ax.plot( | |
| years, | |
| fit_values, | |
| linewidth=2.3, | |
| linestyle="--", | |
| color="#d97706", | |
| label=f"Linear fit: +{slope:,.0f} GB/year", | |
| ) | |
| ax.axvline( | |
| max(observed_years), | |
| color="#6b7280", | |
| linewidth=1.1, | |
| linestyle=":", | |
| ) | |
| ax.annotate( | |
| f"2030 projection: {projected_2030:,.0f} GB", | |
| (2030, projected_2030), | |
| xytext=(-18, 18), | |
| textcoords="offset points", | |
| ha="right", | |
| va="bottom", | |
| fontsize=9, | |
| arrowprops=dict(arrowstyle="-", color="#6b7280", lw=0.8), | |
| bbox=dict(boxstyle="round,pad=0.22", fc="white", ec="#e5e7eb", alpha=0.9), | |
| ) | |
| ax.scatter( | |
| [int(row["year"]) for row in projection_rows], | |
| [float(row["linear_fit_memory_gb"]) for row in projection_rows], | |
| color="#d97706", | |
| s=48, | |
| zorder=4, | |
| ) | |
| ax.set_title( | |
| "Under $3,000 best-so-far memory with linear projection to 2030", | |
| fontsize=15, | |
| pad=18, | |
| ) | |
| ax.set_xlabel("Year") | |
| ax.set_ylabel("Memory capacity (GB)") | |
| ax.set_xticks(list(range(min(years), max(years) + 1))) | |
| ax.set_xlim(min(years) - 0.35, max(years) + 0.35) | |
| ax.set_ylim(0, max(max(observed_values), max(fit_values)) * 1.22) | |
| ax.grid(axis="y", alpha=0.25) | |
| ax.legend(loc="upper left") | |
| fig.text( | |
| 0.5, | |
| 0.015, | |
| "Projection is a linear extrapolation of the 2020-2026 under-$3k best-so-far memory frontier, not verified future product data.", | |
| ha="center", | |
| va="bottom", | |
| fontsize=8.2, | |
| ) | |
| fig.tight_layout(rect=(0.03, 0.08, 0.98, 0.95)) | |
| chart_path = PROJECTION_CHARTS_DIR / "under_3000_memory_linear_projection.png" | |
| output_paths = save_chart(fig, chart_path) | |
| plt.close(fig) | |
| return output_paths | |
| under_3000_projection_chart_path, under_3000_projection_chart_svg_path = ( | |
| plot_under_3000_memory_projection(under_3000_projection_rows) | |
| ) | |
| best_price_per_memory_frontier, best_price_per_memory_events = derive_best_metric_frontier( | |
| metric_key="price_per_memory_gb", | |
| metric_name="best_price_per_memory", | |
| lower_is_better=True, | |
| ) | |
| write_metric_csv( | |
| best_price_per_memory_frontier, | |
| best_price_per_memory_events, | |
| frontier_path=DATA_DIR / "best_price_per_memory_2020_2026.csv", | |
| events_path=DATA_DIR / "best_price_per_memory_events_2020_2026.csv", | |
| ) | |
| best_price_per_memory_chart_paths, best_price_per_memory_chart_svg_paths = ( | |
| plot_best_metric_frontier( | |
| best_price_per_memory_frontier, | |
| best_price_per_memory_events, | |
| output_dir=PRICE_PER_MEMORY_BEST_CHARTS_DIR, | |
| filename_suffix="price_per_memory_best_so_far", | |
| title_prefix="Best-so-far complete-system price per GB of accelerator memory", | |
| ylabel="USD per GB", | |
| metric_key="price_per_memory_gb", | |
| metric_formatter=lambda value: f"${value:,.2f}/GB", | |
| footer=( | |
| "Lower is better. Best-so-far frontier carries the previous lowest " | |
| "complete-system USD/GB forward until a lower point appears." | |
| ), | |
| ) | |
| ) | |
| best_bandwidth_frontier, best_bandwidth_events = derive_best_metric_frontier( | |
| metric_key="bandwidth_gbps", | |
| metric_name="best_bandwidth", | |
| lower_is_better=False, | |
| ) | |
| write_metric_csv( | |
| best_bandwidth_frontier, | |
| best_bandwidth_events, | |
| frontier_path=DATA_DIR / "best_bandwidth_2020_2026.csv", | |
| events_path=DATA_DIR / "best_bandwidth_events_2020_2026.csv", | |
| ) | |
| best_bandwidth_chart_paths, best_bandwidth_chart_svg_paths = plot_best_metric_frontier( | |
| best_bandwidth_frontier, | |
| best_bandwidth_events, | |
| output_dir=BANDWIDTH_BEST_CHARTS_DIR, | |
| filename_suffix="bandwidth_best_so_far", | |
| title_prefix="Best-so-far peak memory bandwidth in one machine", | |
| ylabel="Peak memory bandwidth (GB/s)", | |
| metric_key="bandwidth_gbps", | |
| metric_formatter=lambda value: f"{value:,.0f} GB/s", | |
| footer=( | |
| "Higher is better. Best-so-far frontier carries the previous highest " | |
| "source-backed bandwidth forward until a higher point appears." | |
| ), | |
| ) | |
| readme = f"""Single-machine accelerator-memory frontier, 2020–2026 | |
| ========================================================= | |
| Scope | |
| ----- | |
| - Complete laptops, desktops, towers, compact workstations, and deskside AI systems. | |
| - One physical machine only; no networked clusters or rack-scale systems. | |
| - Producers: Apple, NVIDIA, AMD. | |
| - Prices are nominal US dollars at the documented date. | |
| - A configuration is assigned using the price of the complete configured machine, | |
| not the GPU card's price or the product family's base price. | |
| - Storage or unrelated accessories were not added merely to force a machine into | |
| a more expensive band. | |
| Memory definitions | |
| ------------------ | |
| U = unified/coherent memory accessible by the integrated accelerator. | |
| V = VRAM on one discrete GPU. | |
| Σ = aggregate installed VRAM across multiple GPUs in one chassis. This is not | |
| necessarily one contiguous memory pool for a single process. | |
| Bandwidth definitions | |
| --------------------- | |
| Bandwidth values are stored in data/items/*.json under memory.bandwidth_gbps. | |
| For U, the number is the reported unified/coherent memory bandwidth. For V, it | |
| is the reported single-GPU VRAM bandwidth. For Σ, it is the arithmetic aggregate | |
| of the installed GPUs and the item must carry a bandwidth_note. Tiered coherent | |
| memory systems also carry a note explaining which tier is plotted. | |
| Evidence | |
| -------- | |
| quoted = complete-system price/configuration directly documented. | |
| reconstructed = contemporaneous vendor configuration rebuilt from required options. | |
| bom_inferred = the price band is supported by a documented component BOM, but an | |
| exact complete-system checkout/quote was not available. These | |
| points are hollow and marked * in the charts. | |
| Interpretation | |
| -------------- | |
| The editable source of truth is data/items/*.json. The generator expands each | |
| item price_history entry into the CSV source catalog, then derives the annual | |
| and monotonic frontiers from that catalog. The charts show the maximum | |
| documented configuration for each producer, year, and price band. They are a | |
| reproducible documented frontier, not a claim that every worldwide boutique or | |
| private custom build was enumerated. | |
| Extracted memory pricing | |
| ------------------------ | |
| The complete-system price_per_memory_gb metric is still total machine price | |
| divided by accelerator-accessible memory. The separate data/memory_price_sources.csv | |
| file stores source-backed GPU/card prices or same-device memory upgrade deltas. | |
| The generator expands it into data/memory_price_per_gb.csv. That extracted metric | |
| is useful for component economics, but it is not a complete-machine affordability | |
| metric. | |
| Projection charts | |
| ----------------- | |
| data/consumer_memory_linear_projection_2020_2030.csv and the matching projection | |
| chart fit one least-squares linear trend per under-$10,000 consumer price band, | |
| then extend those fitted lines through 2030. | |
| data/under_3000_memory_linear_projection_2020_2030.csv keeps the focused | |
| under-$3,000 projection as a compatibility output and separate detail chart. Both | |
| projection views are explicit scenario assumptions, not source-backed future | |
| product data. | |
| Important Apple correction | |
| -------------------------- | |
| The 512GB M3 Ultra Mac Studio was a real 2025 launch configuration and belongs in | |
| the $5,000–$10,000 band at about $9,499. It is not the current 2026 Mac Studio | |
| configuration. Apple's current US specifications list the M3 Ultra at 96GB with | |
| memory not configurable; the current 128GB Apple point in the $5,000–$10,000 band | |
| comes from the M5 Max MacBook Pro. | |
| Generated files | |
| --------------- | |
| - data/items/*.json: editable complete-system item records with price history | |
| - data/memory_price_sources.csv: source-backed GPU/card or same-device memory upgrade prices | |
| - data/memory_price_per_gb.csv: generated extracted-memory USD/GB rows | |
| - data/consumer_memory_linear_projection_2020_2030.csv: linear projections | |
| for the under-$10,000 consumer best-so-far memory frontiers | |
| - data/under_3000_memory_linear_projection_2020_2030.csv: linear projection | |
| for the under-$3,000 best-so-far memory frontier | |
| - ITEM_FORMAT.md and schemas/local_hardware_item.schema.json: item format docs | |
| - verified_system_catalog.csv: every source configuration and evidence grade | |
| - annual_frontier_2020_2026.csv: selected annual maximums | |
| - dominant_frontier_2020_2026.csv: monotonic best-so-far frontier by price band | |
| - dominant_frontier_events_2020_2026.csv: releases and price updates labeled on | |
| monotonic charts | |
| - best_price_per_memory_2020_2026.csv and best_price_per_memory_events_2020_2026.csv: | |
| lowest complete-system USD/GB frontier and labeled events | |
| - best_bandwidth_2020_2026.csv and best_bandwidth_events_2020_2026.csv: | |
| highest source-backed memory-bandwidth frontier and labeled events | |
| - frontier_matrix.csv: compact chart-value matrix | |
| - six producer comparison PNG/SVG chart pairs, one per price band | |
| - space/: static Hugging Face Space bundle generated by scripts/build_static_space.py | |
| - one combined consumer-band best-so-far memory PNG/SVG chart pair | |
| - two linear projection PNG/SVG chart pairs through 2030 | |
| - six monotonic best-so-far PNG/SVG chart pairs, one per price band | |
| - six price-per-memory PNG/SVG chart pairs, one per price band | |
| - six memory-bandwidth PNG/SVG chart pairs, one per price band | |
| - six best-so-far price-per-memory PNG/SVG chart pairs, one per price band | |
| - six best-so-far memory-bandwidth PNG/SVG chart pairs, one per price band | |
| """ | |
| readme_path = ROOT / "README_methodology.txt" | |
| readme_path.write_text(readme, encoding="utf-8") | |
| # Build a simple HTML gallery without combining charts into a subplot. | |
| html_parts = [ | |
| "<!doctype html><html><head><meta charset='utf-8'>", | |
| "<title>Single-machine memory frontier by price band</title>", | |
| "<style>body{font-family:system-ui,sans-serif;max-width:1200px;margin:30px auto;padding:0 18px}" | |
| "img{width:100%;height:auto;border:1px solid #ddd;margin:12px 0 34px}" | |
| "h1{font-size:1.7rem}h2{margin-top:34px}p{line-height:1.45}</style></head><body>", | |
| "<h1>Single-machine accelerator-memory frontier, 2020–2026</h1>", | |
| "<p>Apple, NVIDIA, and AMD are plotted separately. The monotonic charts also show the best-so-far frontier for each price band, including releases and price updates that match the frontier without raising it. See README_methodology.txt and the CSV files for definitions, sources, and evidence grades.</p>", | |
| "<h2>Producer comparison charts</h2>" | |
| ] | |
| for chart, svg in zip(chart_paths, chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/{chart.name}'>PNG</a> · <a href='../charts/{svg.name}'>SVG</a></p>") | |
| html_parts.append("<h2>Price per GB charts</h2>") | |
| for chart, svg in zip(price_per_memory_chart_paths, price_per_memory_chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/price_per_memory/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/price_per_memory/{chart.name}'>PNG</a> · <a href='../charts/price_per_memory/{svg.name}'>SVG</a></p>") | |
| html_parts.append("<h2>Memory bandwidth charts</h2>") | |
| for chart, svg in zip(bandwidth_chart_paths, bandwidth_chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/bandwidth/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/bandwidth/{chart.name}'>PNG</a> · <a href='../charts/bandwidth/{svg.name}'>SVG</a></p>") | |
| html_parts.append("<h2>Monotonic best-so-far charts</h2>") | |
| html_parts.append( | |
| f"<img src='../charts/monotonic/{consumer_monotonic_chart_svg_path.name}' " | |
| f"alt='{consumer_monotonic_chart_svg_path.stem}'>" | |
| ) | |
| html_parts.append( | |
| f"<p><a href='../charts/monotonic/{consumer_monotonic_chart_path.name}'>PNG</a> · " | |
| f"<a href='../charts/monotonic/{consumer_monotonic_chart_svg_path.name}'>SVG</a></p>" | |
| ) | |
| for chart, svg in zip(monotonic_chart_paths, monotonic_chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/monotonic/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/monotonic/{chart.name}'>PNG</a> · <a href='../charts/monotonic/{svg.name}'>SVG</a></p>") | |
| html_parts.append("<h2>Projection charts</h2>") | |
| html_parts.append( | |
| f"<img src='../charts/projections/{consumer_projection_chart_svg_path.name}' " | |
| f"alt='{consumer_projection_chart_svg_path.stem}'>" | |
| ) | |
| html_parts.append( | |
| f"<p><a href='../charts/projections/{consumer_projection_chart_path.name}'>PNG</a> · " | |
| f"<a href='../charts/projections/{consumer_projection_chart_svg_path.name}'>SVG</a></p>" | |
| ) | |
| html_parts.append( | |
| f"<img src='../charts/projections/{under_3000_projection_chart_svg_path.name}' " | |
| f"alt='{under_3000_projection_chart_svg_path.stem}'>" | |
| ) | |
| html_parts.append( | |
| f"<p><a href='../charts/projections/{under_3000_projection_chart_path.name}'>PNG</a> · " | |
| f"<a href='../charts/projections/{under_3000_projection_chart_svg_path.name}'>SVG</a></p>" | |
| ) | |
| html_parts.append("<h2>Best-so-far price per GB charts</h2>") | |
| for chart, svg in zip(best_price_per_memory_chart_paths, best_price_per_memory_chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/best_so_far_price_per_memory/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/best_so_far_price_per_memory/{chart.name}'>PNG</a> · <a href='../charts/best_so_far_price_per_memory/{svg.name}'>SVG</a></p>") | |
| html_parts.append("<h2>Best-so-far memory bandwidth charts</h2>") | |
| for chart, svg in zip(best_bandwidth_chart_paths, best_bandwidth_chart_svg_paths): | |
| html_parts.append(f"<img src='../charts/best_so_far_bandwidth/{svg.name}' alt='{svg.stem}'>") | |
| html_parts.append(f"<p><a href='../charts/best_so_far_bandwidth/{chart.name}'>PNG</a> · <a href='../charts/best_so_far_bandwidth/{svg.name}'>SVG</a></p>") | |
| html_parts.append("</body></html>") | |
| html_path = GALLERY_DIR / "all_price_band_charts.html" | |
| html_path.write_text("\n".join(html_parts), encoding="utf-8") | |
| # ZIP the complete package. | |
| zip_path = ROOT / "local_hardware_frontier_artifacts.zip" | |
| with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: | |
| for directory in (DATA_DIR, CHARTS_DIR, GALLERY_DIR): | |
| for path in sorted(p for p in directory.rglob("*") if p.is_file()): | |
| zf.write(path, arcname=str(path.relative_to(ROOT))) | |
| for directory in (ROOT / "schemas",): | |
| for path in sorted(p for p in directory.rglob("*") if p.is_file()): | |
| zf.write(path, arcname=str(path.relative_to(ROOT))) | |
| for path in [ | |
| ROOT / "ITEM_FORMAT.md", | |
| ROOT / "README.md", | |
| readme_path, | |
| ROOT / "requirements.txt", | |
| ROOT / "scripts" / "local_hardware_items.py", | |
| ROOT / "scripts" / "validate_items.py", | |
| ROOT / "scripts" / "generate_graphs.py", | |
| ROOT / "scripts" / "build_self_contained_md.py", | |
| ROOT / "scripts" / "build_static_space.py", | |
| ]: | |
| if path.exists(): | |
| zf.write(path, arcname=str(path.relative_to(ROOT))) | |
| print( | |
| f"Created {len(chart_paths)} comparison chart pairs, " | |
| f"{len(monotonic_chart_paths) + 1} monotonic chart pairs, " | |
| f"{len(price_per_memory_chart_paths)} price-per-memory chart pairs, " | |
| f"{len(bandwidth_chart_paths)} bandwidth chart pairs, " | |
| f"{len(best_price_per_memory_chart_paths)} best-price chart pairs, " | |
| f"{len(best_bandwidth_chart_paths)} best-bandwidth chart pairs, " | |
| "2 projection chart pairs, " | |
| f"{len(memory_price_rows)} extracted memory price rows, and source data." | |
| ) | |
| print(f"ZIP: {zip_path}") | |
| for p in ( | |
| chart_paths | |
| + chart_svg_paths | |
| + [consumer_monotonic_chart_path] | |
| + [consumer_monotonic_chart_svg_path] | |
| + [consumer_projection_chart_path] | |
| + [consumer_projection_chart_svg_path] | |
| + [under_3000_projection_chart_path] | |
| + [under_3000_projection_chart_svg_path] | |
| + monotonic_chart_paths | |
| + monotonic_chart_svg_paths | |
| + price_per_memory_chart_paths | |
| + price_per_memory_chart_svg_paths | |
| + bandwidth_chart_paths | |
| + bandwidth_chart_svg_paths | |
| + best_price_per_memory_chart_paths | |
| + best_price_per_memory_chart_svg_paths | |
| + best_bandwidth_chart_paths | |
| + best_bandwidth_chart_svg_paths | |
| ): | |
| print(p) | |
Xet Storage Details
- Size:
- 66.1 kB
- Xet hash:
- 7963192f152078861878ead36fa503f3355ce1e83082af4c083c65be69f14304
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.