diff --git a/antenna/DIAGRAM_INTEGRATION_NOTES.md b/antenna/DIAGRAM_INTEGRATION_NOTES.md
new file mode 100644
index 0000000..25158c7
--- /dev/null
+++ b/antenna/DIAGRAM_INTEGRATION_NOTES.md
@@ -0,0 +1,42 @@
+# Antenna Diagram Integration Notes
+
+Created for `UWS-124` as reusable SVG assets for the Antenna SciPy presentation and A0 poster.
+
+## Asset Path
+
+Use the SVGs from `antenna/assets/diagrams/`:
+
+- `antenna-a-push-model-before.svg` — place with the architecture problem statement for the original push flow.
+- `antenna-b-pull-model-after.svg` — place immediately after A to explain the pull-based target architecture and horizontal scaling.
+- `antenna-c-gpu-utilization-before.svg` — place before GPU optimization details to make idle time concrete.
+- `antenna-d-cpu-gpu-data-flow.svg` — place where the PIL/CPU materialization bottleneck is introduced.
+- `antenna-e-cuda-stream-pipeline.svg` — place with CUDA stream/overlap explanation.
+- `antenna-f-gpu-utilization-after.svg` — place after E as the visual mirror of C and the payoff slide.
+
+## Design Notes
+
+- SVGs use `viewBox="0 0 1600 900"` for 16:9 slides and scalable poster placement.
+- Primary diagram color is the requested deep blue `#1A5276`; accents are UW SSEC-compatible gold `#85754d`/`#b7a57a` and teal `#2ad2c9`.
+- Typography follows UW SSEC presentation conventions: Encode Sans for headings/UI labels and Open Sans for supporting text, with system fallbacks.
+- Each SVG includes `title`, `desc`, and `role="img"` for accessible embedding.
+
+## Implementation Guidance
+
+For Reveal.js slides, prefer direct image embedding at full content width:
+
+```html
+
+```
+
+Suggested CSS:
+
+```css
+.diagram-full {
+ display: block;
+ inline-size: 100%;
+ max-block-size: 72vh;
+ object-fit: contain;
+}
+```
+
+No content placeholders remain in the SVGs. The only dependency is deck-owner confirmation that `antenna/assets/diagrams/` is the final path for the Antenna deck.
diff --git a/antenna/assets/antenna-screenshot-crop.png b/antenna/assets/antenna-screenshot-crop.png
new file mode 100644
index 0000000..b88c386
Binary files /dev/null and b/antenna/assets/antenna-screenshot-crop.png differ
diff --git a/antenna/assets/antenna-screenshot.png b/antenna/assets/antenna-screenshot.png
new file mode 100644
index 0000000..8e152c4
Binary files /dev/null and b/antenna/assets/antenna-screenshot.png differ
diff --git a/antenna/assets/diagrams/after-simple.png b/antenna/assets/diagrams/after-simple.png
new file mode 100644
index 0000000..0d505b4
Binary files /dev/null and b/antenna/assets/diagrams/after-simple.png differ
diff --git a/antenna/assets/diagrams/antenna-a-push-model-before.eraserdiagram b/antenna/assets/diagrams/antenna-a-push-model-before.eraserdiagram
new file mode 100644
index 0000000..5edd140
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-a-push-model-before.eraserdiagram
@@ -0,0 +1,16 @@
+// A. Push Model Before: Synchronous Endpoint
+// Antenna initiates work and blocks while the public processing service runs.
+
+colorMode pastel
+typeface clean
+
+Antenna [color: blue, label: "Antenna\njob orchestration"]
+Public Endpoint [color: orange, label: "Public Endpoint\nProcessing Service"]
+Worker Runtime [color: blue, label: "Worker Runtime\nservice-owned"]
+
+Antenna > Public Endpoint: POST job payload
+activate Antenna
+Public Endpoint > Worker Runtime: Start processing
+Worker Runtime > Public Endpoint: Result returned
+Public Endpoint > Antenna: Synchronous response
+deactivate Antenna
diff --git a/antenna/assets/diagrams/antenna-a-push-model-before.png b/antenna/assets/diagrams/antenna-a-push-model-before.png
new file mode 100644
index 0000000..d000776
Binary files /dev/null and b/antenna/assets/diagrams/antenna-a-push-model-before.png differ
diff --git a/antenna/assets/diagrams/antenna-a-push-model-before.svg b/antenna/assets/diagrams/antenna-a-push-model-before.svg
new file mode 100644
index 0000000..499fd2f
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-a-push-model-before.svg
@@ -0,0 +1,29 @@
+
+
+
+
+ eraser.io
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/antenna/assets/diagrams/antenna-a-push-model-simple.svg b/antenna/assets/diagrams/antenna-a-push-model-simple.svg
new file mode 100644
index 0000000..75f341a
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-a-push-model-simple.svg
@@ -0,0 +1,29 @@
+
+
+
+
+ eraser.io
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/antenna/assets/diagrams/antenna-b-pull-model-after.eraserdiagram b/antenna/assets/diagrams/antenna-b-pull-model-after.eraserdiagram
new file mode 100644
index 0000000..4068bf0
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-b-pull-model-after.eraserdiagram
@@ -0,0 +1,21 @@
+// B. Pull Model After: Workers Scale Horizontally
+// Antenna exposes job state; workers pull batches when they have local capacity.
+
+direction right
+colorMode pastel
+typeface clean
+
+Antenna [color: blue, label: "Antenna\njob source of truth"]
+
+Jobs API [color: orange, label: "Jobs API\npoll, lease, update"] {
+ Queue [shape: rectangle, color: orange, label: "batched work queue"]
+}
+
+Worker A [color: blue, label: "Worker A\nlocal processing"]
+Worker B [color: blue, label: "Worker B\nlocal processing"]
+
+Antenna > Jobs API: publish jobs
+Worker A > Jobs API: poll + pull batch
+Jobs API > Worker A: post results
+Worker B > Jobs API: poll + pull batch
+Jobs API > Worker B: post results
diff --git a/antenna/assets/diagrams/antenna-b-pull-model-after.svg b/antenna/assets/diagrams/antenna-b-pull-model-after.svg
new file mode 100644
index 0000000..060bca1
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-b-pull-model-after.svg
@@ -0,0 +1,29 @@
+
+
+
+
+ eraser.io
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/antenna/assets/diagrams/antenna-c-gpu-utilization-before.eraserdiagram b/antenna/assets/diagrams/antenna-c-gpu-utilization-before.eraserdiagram
new file mode 100644
index 0000000..899a8fa
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-c-gpu-utilization-before.eraserdiagram
@@ -0,0 +1,31 @@
+// C. GPU Utilization Before Optimization
+// CPU-side image conversion and transfer setup leave the GPU waiting between kernels.
+
+direction right
+colorMode bold
+typeface clean
+
+CPU [color: blue, label: "CPU"] {
+ PIL convert [color: blue, label: "PIL convert"]
+ Host copy 1 [color: blue, label: "host copy"]
+ Prepare [color: blue, label: "prepare"]
+ Host copy 2 [color: blue, label: "host copy"]
+
+ PIL convert > Host copy 1 > Prepare > Host copy 2
+}
+
+GPU [label: "GPU"] {
+ GPU idle 1 [color: orange, label: "idle"]
+ GPU kernel 1 [color: "#2ad2c9", label: "kernel"]
+ GPU idle 2 [color: orange, label: "idle"]
+ GPU kernel 2 [color: "#2ad2c9", label: "kernel"]
+ GPU idle 3 [color: orange, label: "idle"]
+
+ GPU idle 1 > GPU kernel 1 > GPU idle 2 > GPU kernel 2 > GPU idle 3
+}
+
+legend [position: bottom-left] {
+ [color: blue, label: CPU setup]
+ [color: "#2ad2c9", label: GPU work]
+ [color: orange, label: GPU idle]
+}
diff --git a/antenna/assets/diagrams/antenna-c-gpu-utilization-before.svg b/antenna/assets/diagrams/antenna-c-gpu-utilization-before.svg
new file mode 100644
index 0000000..9f6fb30
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-c-gpu-utilization-before.svg
@@ -0,0 +1,39 @@
+
+ GPU utilization before optimization
+ CPU and GPU timeline showing large GPU idle gaps before optimization.
+
+
+
+
+
+ C. GPU Utilization Before Optimization
+ CPU-side image conversion and transfer setup leave the GPU waiting between kernels.
+
+
+ CPU
+ GPU
+
+
+
+
+ 0 20 40 60 80 100 ms
+
+
+ PIL convert host copy prepare host copy
+
+
+ idle kernel idle kernel idle
+
+
+
+
+ Low duty cycle, high wait time
+
+
+ CPU setup
+ GPU work
+ GPU idle
+
+
diff --git a/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.eraserdiagram b/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.eraserdiagram
new file mode 100644
index 0000000..76fcb84
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.eraserdiagram
@@ -0,0 +1,22 @@
+// D. CPU-GPU Data Flow
+// The optimized path removes CPU image materialization from the hot loop.
+
+direction right
+colorMode pastel
+typeface clean
+
+Before [color: orange, label: "Before: device data leaves the GPU"] {
+ GPU array before [color: "#2ad2c9", label: "GPU array"]
+ PIL Image [color: orange, label: "PIL Image\n(CPU memory)"]
+ GPU kernel before [color: "#2ad2c9", label: "GPU\nkernel input"]
+
+ GPU array before > PIL Image: extra copies
+ PIL Image > GPU kernel before: synchronization
+}
+
+After [color: blue, label: "After: direct GPU-to-GPU path"] {
+ GPU source [color: "#2ad2c9", label: "GPU\nsource array"]
+ GPU kernel after [color: "#2ad2c9", label: "GPU\nkernel input"]
+
+ GPU source > GPU kernel after: no CPU materialization
+}
diff --git a/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.svg b/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.svg
new file mode 100644
index 0000000..eff15f9
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-d-cpu-gpu-data-flow.svg
@@ -0,0 +1,56 @@
+
+ CPU-GPU data flow before and after optimization
+ Before path routes through PIL Image on CPU before returning to GPU; after path keeps data on GPU directly.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Before: device data leaves the GPU
+
+ GPU
+ array
+
+ PIL Image
+ CPU memory
+
+ GPU
+ kernel input
+
+
+
+ extra copies + synchronization
+
+
+
+
+ After: direct GPU-to-GPU path
+
+ GPU
+ source array
+
+ GPU
+ kernel input
+
+
+ no CPU materialization
+
+
+
+
diff --git a/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.eraserdiagram b/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.eraserdiagram
new file mode 100644
index 0000000..ee31415
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.eraserdiagram
@@ -0,0 +1,40 @@
+// E. CUDA Stream Pipeline
+// Streams overlap transfer and compute so later batches begin before earlier batches finish.
+
+direction right
+colorMode bold
+typeface clean
+
+Before [label: "Before: sequential work"] {
+ Single stream [label: "Single stream"] {
+ T1 before [color: blue, label: "T1\nTransfer"]
+ C1 before [color: "#2ad2c9", label: "C1\nCompute"]
+ T2 before [color: blue, label: "T2\nTransfer"]
+ C2 before [color: "#2ad2c9", label: "C2\nCompute"]
+ Wait [color: orange, label: "wait"]
+
+ T1 before > C1 before > T2 before > C2 before > Wait
+ }
+}
+
+After [label: "After: overlapped streams"] {
+ Stream 1 [label: "Stream 1"] {
+ S1 T1 [color: blue, label: "T1"]
+ S1 C1 [color: "#2ad2c9", label: "C1"]
+ S1 T3 [color: blue, label: "T3"]
+
+ S1 T1 > S1 C1 > S1 T3
+ }
+
+ Stream 2 [label: "Stream 2"] {
+ S2 T2 [color: blue, label: "T2"]
+ S2 C2 [color: "#2ad2c9", label: "C2"]
+
+ S2 T2 > S2 C2
+ }
+}
+
+legend [position: bottom-left] {
+ [color: blue, label: Transfer]
+ [color: "#2ad2c9", label: Compute]
+}
diff --git a/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.svg b/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.svg
new file mode 100644
index 0000000..a56bbf0
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-e-cuda-stream-pipeline.svg
@@ -0,0 +1,58 @@
+
+ CUDA stream pipeline before and after optimization
+ Comparison of sequential transfer and compute against overlapped transfer and compute across CUDA streams.
+
+
+
+
+
+ E. CUDA Stream Pipeline
+ Streams overlap transfer and compute so later batches begin before earlier batches finish.
+
+
+
+ Before: sequential work
+ Single stream
+
+
+ T1
+ C1
+ T2
+ C2
+ wait
+
+
+
+ GPU occupancy
+
+ idle compute idle compute
+
+
+
+ After: overlapped streams
+ Stream 1
+ Stream 2
+
+
+
+ T1
+ C1
+ T2
+ C2
+ T3
+
+
+
+ GPU occupancy
+
+
+ near-continuous compute window
+
+
+
+ Transfer
+ Compute
+
+
diff --git a/antenna/assets/diagrams/antenna-f-gpu-utilization-after.eraserdiagram b/antenna/assets/diagrams/antenna-f-gpu-utilization-after.eraserdiagram
new file mode 100644
index 0000000..d02c116
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-f-gpu-utilization-after.eraserdiagram
@@ -0,0 +1,33 @@
+// F. GPU Utilization After Optimization
+// Direct GPU data flow and stream overlap keep compute work queued with only small gaps.
+
+direction right
+colorMode bold
+typeface clean
+
+CPU [color: blue, label: "CPU"] {
+ Enqueue 1 [color: blue, label: "enqueue"]
+ Enqueue 2 [color: blue, label: "enqueue"]
+ Enqueue 3 [color: blue, label: "enqueue"]
+ Enqueue 4 [color: blue, label: "enqueue"]
+
+ Enqueue 1 > Enqueue 2 > Enqueue 3 > Enqueue 4
+}
+
+GPU [label: "GPU"] {
+ Kernel batch 1 [color: "#2ad2c9", label: "kernel batch 1"]
+ Handoff 1 [color: blue, label: "gap"]
+ Kernel batch 2 [color: "#2ad2c9", label: "kernel batch 2"]
+ Handoff 2 [color: blue, label: "gap"]
+ Kernel batch 3 [color: "#2ad2c9", label: "kernel batch 3"]
+ Handoff 3 [color: blue, label: "gap"]
+ Batch 4 [color: "#2ad2c9", label: "batch 4"]
+
+ Kernel batch 1 > Handoff 1 > Kernel batch 2 > Handoff 2 > Kernel batch 3 > Handoff 3 > Batch 4
+}
+
+legend [position: bottom-left] {
+ [color: blue, label: "CPU enqueue"]
+ [color: "#2ad2c9", label: "GPU work"]
+ [color: blue, label: "handoff gap"]
+}
diff --git a/antenna/assets/diagrams/antenna-f-gpu-utilization-after.svg b/antenna/assets/diagrams/antenna-f-gpu-utilization-after.svg
new file mode 100644
index 0000000..cea66f0
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-f-gpu-utilization-after.svg
@@ -0,0 +1,39 @@
+
+ GPU utilization after optimization
+ CPU and GPU timeline showing near-continuous GPU utilization after optimization.
+
+
+
+
+
+ F. GPU Utilization After Optimization
+ Direct GPU data flow and stream overlap keep compute work queued with only small gaps.
+
+
+ CPU
+ GPU
+
+
+
+
+ 0 20 40 60 80 100 ms
+
+
+ enqueue enqueue enqueue enqueue
+
+
+ kernel batch 1 kernel batch 2 kernel batch 3 batch 4
+
+
+
+
+ High duty cycle, small gaps
+
+
+ CPU enqueue
+ GPU work
+ handoff gap
+
+
diff --git a/antenna/assets/diagrams/antenna-orchestration.html b/antenna/assets/diagrams/antenna-orchestration.html
new file mode 100644
index 0000000..0d88535
--- /dev/null
+++ b/antenna/assets/diagrams/antenna-orchestration.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+Antenna — model orchestration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Antenna
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ University A
+ hosted model · REST API
+
+
+
+ Institute B
+ hosted model · REST API
+
+
+
+ University C
+ hosted model · REST API
+
+
+
+
+
+
+
diff --git a/antenna/assets/diagrams/before-simple.png b/antenna/assets/diagrams/before-simple.png
new file mode 100644
index 0000000..106fabd
Binary files /dev/null and b/antenna/assets/diagrams/before-simple.png differ
diff --git a/antenna/assets/diagrams/dataloader-diagram.html b/antenna/assets/diagrams/dataloader-diagram.html
new file mode 100644
index 0000000..ff5df66
--- /dev/null
+++ b/antenna/assets/diagrams/dataloader-diagram.html
@@ -0,0 +1,167 @@
+
+
+
+
+
+PyTorch DataLoader — inference pipeline
+
+
+
+
+
+
+
+
+ PyTorch DataLoader inference pipeline
+ Diagram showing multiple CPU worker processes loading and preprocessing data in parallel, building a prefetch queue of pinned-memory batches that the GPU consumes one at a time for inference.
+
+
+
+
+
+
+
+
+
+ Dataset
+
+
+
+
+
+
+ images
+
+
+
+
+ CPU workers (num_workers=N)
+
+
+
+ Worker 1
+ load · decode · transform
+
+
+
+
+ Worker 2
+ load · decode · transform
+
+
+
+
+
+
+
+
+ Prefetch queue (prefetch_factor)
+
+
+
+ Batch N
+ tensor · pinned memory
+
+
+ Batch N+1
+ tensor · pinned memory
+
+
+ main thread blocks only
+ if queue is empty
+
+
+
+
+
+
+
+
+
+
+ GPU
+ model forward pass
+ inference
+
+
+ processing batch N
+
+ VRAM
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CPU workers
+
+ GPU / queue
+
+ dataset
+
+
+
+
diff --git a/antenna/assets/intro.webp b/antenna/assets/intro.webp
new file mode 100644
index 0000000..18e86d7
Binary files /dev/null and b/antenna/assets/intro.webp differ
diff --git a/antenna/assets/timeline-after.html b/antenna/assets/timeline-after.html
new file mode 100644
index 0000000..05a1fd3
--- /dev/null
+++ b/antenna/assets/timeline-after.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+After — GPU pipeline timeline
+
+
+
+
+ After — pipelined, GPU always busy
+ Four horizontal timelines showing dataloader workers, main thread, async , and GPU after the optimization. GPU runs continuously with no idle time.
+
+
+
+ After — pipelined, GPU always busy
+
+
+ Dataloader workers
+ async
+ Main thread (CPU)
+ GPU
+
+
+
+ fetch + collate
+
+
+ fetch + collate
+
+
+ fetch + collate
+
+
+ fetch + collate
+
+ ···
+
+
+
+
+ to(cuda)
+
+
+
+ to(cuda)
+
+
+
+ post
+
+
+
+ to(cuda)
+
+
+
+ post
+
+
+
+ post
+
+
+
+ model()
+
+
+ model()
+
+
+ model()
+
+ ···
+
+
+
+ detect
+
+
+ classify
+
+
+ detect
+
+
+ classify
+
+
+ detect
+
+ ···
+
+
+
+
+
+ GPU busy continuously — no idle
+
+
+
+
+ time →
+
+
+
+ dataloader workers
+
+
+ main thread
+
+
+ to(cuda)
+
+
+ post
+
+
+ GPU detect
+
+
+ GPU classify
+
+
+
+
diff --git a/antenna/assets/timeline-before.html b/antenna/assets/timeline-before.html
new file mode 100644
index 0000000..cef9944
--- /dev/null
+++ b/antenna/assets/timeline-before.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+ Three horizontal timelines showing dataloader workers, main thread, and GPU before the optimization. GPU and main thread have large red idle segments.
+
+
+
+
+
+
+
+
+
+
+
+
+ Dataloader workers
+ Main thread (CPU)
+ GPU
+
+
+
+ fetch + download B1
+
+
+ idle — not yet asked for B2
+
+
+
+ idle — waiting
+
+
+ collate
+
+
+ .to('cuda')
+
+
+ idle
+
+
+ PIL round-trip ⚠
+
+
+ idle
+
+
+ post
+
+
+ idle
+
+
+
+ idle — waiting for CPU
+
+
+ detect B1
+
+
+ idle — PIL copy
+
+
+ classify B1
+
+
+ idle — waiting for B2
+
+
+
+
+ time →
+
+
+
+ idle / wasted
+
+
+ dataloader workers
+
+
+ main thread
+
+
+ GPU active
+
+
+ PIL round-trip
+
+
+
+
diff --git a/antenna/index.html b/antenna/index.html
new file mode 100644
index 0000000..bc216b0
--- /dev/null
+++ b/antenna/index.html
@@ -0,0 +1,537 @@
+
+
+
+
+
+ Efficient Federated Inference on Entomology Images with PyTorch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SciPy 2026
+ Efficient Federated Inference on Entomology Images with PyTorch
+
+ Carlos Garcia Jurado Suarez
+ UW eScience Institute · Mila Institute
+
+
+
+
+
+
+ What is Antenna?
+
+
Mila Institute's open-source platform — classifying insects from automated wildlife camera-trap images to support biodiversity monitoring at scale.
+
+
+
+
+
+
+
+
+
+
+
+ The Scaling Problem
+
+
+
+
+
+ Integration barrier
+
+
+ Public HTTP endpoint required, a blocker for HPC clusters and university firewalls.
+
+
+
+
+ Throughput ceiling
+
+
+ Synchronous design prevented scaling to large jobs: >300 image jobs would stall.
+
+
+
+
+
+
+
+
+
+
+
+
+ Architecture: Original Push Model
+
+
+
+
+
+
+
+
+ Architecture: Redesigned Pull Model
+
+
Inverting the direction: PS workers poll Antenna's job queue — no public endpoint needed, and each additional worker adds capacity automatically.
+
+
+
+
+
+
+
+
+
+ Architecture: Results
+
+
+
+
+
+ Deploy anywhere
+
+
Workers run anywhere with outbound HTTP: HPC clusters, firewalled networks, local machines.
+
+
+
+ Multi-institution
+
+
Workers can span institutions and different hardware, no uniformity needed.
+
+
+
+ Horizontal scaling
+
+
Add workers without changing Antenna; each independently polls and claims batches.
+
+
+
+
+
+ Inverting push/pull → workers control the speed.
+
+
+
+
+
+
+
+
+
+
+ Optimization: Data Loading
+
+
+
+
+
+
+
+
+
+
+ Optimization: Baseline
+
+
+
1,674s
+
for 1,000 images · RTX A6000 · GPU idle from CPU transfers, sync posting, and suboptimal data loading
+
+
+
+
+
+ Optimization: Fixes
+
+
+
+
+
class RESTDataset(torch.utils.data.IterableDataset):
+# pulls rows from /jobs/{job_id} until exhausted
+# downloads images from S3
+# assembles batches of stacked tensors
+
+
+
+
return torch.utils.data.DataLoader(
+ dataset,
+ batch_size=1, # batches built in the dataset workers, not by DataLoader
+ pin_memory=True, # DMA transfer to GPU
+ collate_fn=_no_op_collate_fn # nothing to collate since each dataset item is already a batch
+ ...)
+
+
torch.cuda.Stream()
+ ...
+ with torch.cuda.stream(self.stream):
+ self.next_batch = v.to(self.device, non_blocking=True)
+
+
torchvision.transforms.ToPILImage()
+
+
ThreadPoolExecutor.submit(...)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Optimization: Results
+
+
+
15×
+
+
faster on a single GPU · RTX A6000
+
+ 1,674s
+ →
+ 108s
+ per 1,000 images · 42K-image job in 77 min
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Links & Q&A
+
+
+
+
Antenna platform
+
insectai.org
+
+
+
Worker implementation & GPU optimizations
+
github.com/RolnickLab/ami-data-companion
+
+
+
Antenna backend (async job queue API)
+
github.com/RolnickLab/antenna
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/antenna/scipy_presentation_outline.md b/antenna/scipy_presentation_outline.md
new file mode 100644
index 0000000..f2345cc
--- /dev/null
+++ b/antenna/scipy_presentation_outline.md
@@ -0,0 +1,228 @@
+# Presentation Outline
+
+**Efficient Federated Inference on Entomology Images with PyTorch**
+
+SciPy 2026 · 30-minute talk · Author: Carlos Garcia Jurado Suarez
+
+---
+
+## Talk Overview
+
+| Time | Section |
+| ------- | ---------------------------------------------------------------- |
+| ~3 min | Introduction — Antenna platform, problem statement, talk roadmap |
+| ~11 min | Part 1 — Async Pull Architecture (Processing Service v2) |
+| ~11 min | Part 2 — PyTorch GPU Inference Optimization |
+| ~5 min | Results, takeaways, Q&A |
+
+---
+
+## Section 1 — Introduction
+
+Goal: orient a generalist Python/data science audience to the domain and motivate both engineering problems. Keep it tight — 3 minutes.
+
+### Slide 1: Title slide `[30 sec]`
+
+- Talk title, speaker name, affiliation (UW eScience Institute / Mila Institute)
+- Conference name and date
+
+> 📝 Clean title slide. No heavy graphics needed. Placeholder for: small Antenna logo or a single camera-trap insect photo as background.
+> Website for context/logo/images: https://www.insectai.org/
+
+---
+
+### Slide 2: What is Antenna? `[1.5 min]`
+
+- Open-source platform from the Mila Institute
+- Processes images from automated wildlife camera traps in the field
+- Classifies insect species using deep learning — supporting biodiversity monitoring at scale
+- Used by multiple research institutions across different deployment environments
+
+> 📝 **PLACEHOLDER:** Screenshot of the Antenna web interface (image catalog view or monitoring dashboard). One clear screenshot is enough. Caption: "Antenna web platform — researchers submit image batches and review classification results."
+>
+> Keep the explanation brief. One sentence on biodiversity monitoring is sufficient — this is context, not the talk topic.
+
+---
+
+### Slide 3: The scaling problem `[1 min]`
+
+- As adoption grew: jobs went from hundreds to millions of images
+- Two interconnected problems emerged:
+ - Third-party researchers couldn't easily integrate their own ML pipelines
+ - The platform couldn't scale processing to large jobs or multiple models
+- This talk: two engineering changes that solve both
+
+> 📝 This slide sets up the two-part structure. Use a simple two-column or two-row layout with the two problems side by side, visually foreshadowing the two parts.
+
+---
+
+## Section 2 — Part 1: Async Pull Architecture
+
+Goal: explain what was broken in the original design and how inverting the communication model fixed it. Conceptual, no deep code.
+
+### Slide 4: How the original system worked `[2 min]`
+
+- Antenna pushed image batches to external ML processing services (PS) over HTTP
+- Each PS had to expose a public HTTP endpoint — hard to deploy in university HPC environments, behind firewalls, etc.
+- Processing was synchronous: Antenna waited for each response before moving on
+- Scalability was entirely the PS implementor's problem
+
+> 📝 **DIAGRAM A — Push model (before).** Mermaid sequence diagram. Flow: Antenna → HTTP push → Processing Service → synchronous response → Antenna. Show that PS requires a public endpoint. Highlight two pain points: (1) arrow from internet to PS labeled "requires public endpoint", (2) blocking return arrow labeled "synchronous — waits for response."
+
+---
+
+### Slide 5: The redesign — pull model `[3 min]`
+
+- Invert the direction: PS workers poll Antenna's job queue and pull task batches on their own schedule
+- PS no longer needs a public endpoint — it initiates all outbound connections
+- Workers can run behind a firewall, on a university HPC cluster, or on a local machine
+- Multiple workers can poll the same queue independently — horizontal scaling falls out naturally
+- Different ML pipelines register with Antenna and pull only jobs matching their pipeline
+
+> 📝 **DIAGRAM B — Pull model (after).** Mermaid sequence diagram. Flow: PS Worker polls Antenna jobs API → Antenna returns task batch → PS processes locally → PS posts results back. Show two worker instances to illustrate horizontal scale. Key contrast: PS worker is now the initiator, not the receiver.
+
+---
+
+### Slide 6: Worker implementation — `ami worker` `[2 min]`
+
+- Ships as `ami worker` CLI command in the `ami-data-companion` package
+- Configuration via environment variables or `.env` file (API URL, auth token, batch size)
+- Worker loop: poll `/jobs` endpoint → fetch task batch → run ML pipeline → POST results back
+- Supports multiple pipelines via; any number of workers can run in parallel
+
+> 📝 **OPTIONAL CODE SNIPPET:** Show the 3-line usage pattern (`export` env vars, `ami worker register`, `ami worker`). Illustrative only — keep font large.
+>
+> No deep architecture diagram needed here. The sequence diagram from Slide 5 carries the conceptual weight. This slide makes it concrete for practitioners.
+
+---
+
+### Slide 7: Part 1 results `[2 min]`
+
+- PS workers can now run anywhere with outbound HTTP — no public endpoint required
+- Horizontal scaling: add more workers without changing Antenna
+- Extensibility: new ML pipelines register and pull independently, researchers can build their own workers
+- Workers can run across multiple research institutions running different hardware configurations
+
+> 📝 If a before/after deployment diagram is available (e.g., showing a university HPC setup that was previously impossible), this is a good place for it. Otherwise keep as a clean bullet summary.
+
+---
+
+## Section 3 — Part 2: PyTorch GPU Inference Optimization
+
+Goal: show the performance problem, walk through the four concrete fixes, and land the 15x number. Balanced between conceptual explanation and practical takeaways.
+
+### Slide 8: The performance baseline `[2 min]`
+
+- Original sync-mode processing service: **1,674 seconds** for 1,000 images on an RTX A6000
+- GPU utilization was poor — significant idle gaps visible in profiling
+- Root causes: CPU-GPU transfer bottlenecks, synchronous result posting, suboptimal data loading
+
+> 📝 **DIAGRAM C — GPU utilization before optimization.** Horizontal timeline (Gantt style). Two rows: GPU and CPU. Show large idle gaps on the GPU row. If a real `nvidia-smi` or PyTorch profiler screenshot is available from PR #122, use that as reference or include directly.
+>
+> The 1,674 second number is the anchor. Make it large and typographically prominent on this slide.
+
+---
+
+### Slide 9: Fix 1 — Keep data on the GPU between stages `[2 min]`
+
+- Detection and classification are two sequential model passes
+- Original code converted intermediate tensors back to PIL images between passes
+- This forced a GPU → CPU → GPU round-trip transfer for every image in the batch
+- Fix: keep tensors on GPU and pass them directly from detector to classifier
+
+> 📝 **DIAGRAM D — CPU-GPU data flow before/after.** Two rows. Before: `GPU → PIL Image (CPU) → GPU`. After: `GPU → GPU` (direct). Use color to highlight the unnecessary CPU hop in the before row.
+
+---
+
+### Slide 10: Fix 2 — Pinned memory and batch collation `[2 min]`
+
+- `pin_memory=True` in the DataLoader enables DMA transfers — CPU memory is page-locked, avoiding an extra copy on the way to the GPU
+- Batch collation (stacking images into a tensor) was happening in the main process
+- For large images this stacking is slow and blocks the GPU pipeline
+- Fix: move collation into DataLoader workers so it runs in parallel with GPU computation
+
+> 📝 No diagram needed — bullet points are sufficient. Optional: one-line code snippet showing `pin_memory=True` in the DataLoader constructor. Keep font large.
+
+---
+
+### Slide 11: Fix 3 — CUDA streams for prefetching `[2 min]`
+
+- GPU was idle waiting for the next batch to transfer from CPU
+- CUDA streams allow concurrent execution: stream A processes batch N while stream B prefetches batch N+1 into GPU memory
+- Result: near-continuous GPU utilization instead of alternating compute/transfer cycles
+
+> 📝 **DIAGRAM E — CUDA stream pipeline before/after.** Two timelines. Before: Transfer → Compute → Transfer → Compute (sequential, with gaps). After: overlapping Transfer and Compute using two streams. A Gantt-style diagram works well.
+
+---
+
+### Slide 12: Fix 4 — Async result posting `[1.5 min]`
+
+- After each batch, results were posted to Antenna synchronously — GPU sat idle during the HTTP request
+- Fix: `ResultPoster` runs in a background thread
+- Worker loads and processes batch N+1 while batch N results are being posted
+
+> 📝 This fix is the same principle as CUDA streams applied at the API layer — overlap I/O with compute. No separate diagram needed if Diagram E already illustrates the concept.
+
+---
+
+### Slide 13: Combined results `[2 min]`
+
+- After all four optimizations: **108 seconds** for 1,000 images — a **15x improvement** on a single GPU
+- 42,000-image job completed in 77 minutes with no interruptions
+- Async architecture adds a further multiplier: multiple workers scale linearly
+- Key insight: The system has multiple resources: GPU, CPU, Network and GPU/CPU bus - keep them all busy
+
+> 📝 **DIAGRAM F — GPU utilization after optimization.** Mirror of Diagram C. Near-continuous GPU utilization. Use a real profiler screenshot from PR #122 if available.
+>
+> **OPTIONAL:** Side-by-side bar chart showing 1,674s vs. 108s.
+>
+> This is the climax slide of the talk. Give the **15x** number maximum visual weight — consider large-text treatment with the two raw numbers below it.
+
+---
+
+## Section 4 — Conclusion & Takeaways
+
+### Slide 14: Summary and generalization `[2 min]`
+
+- Part 1 takeaway: inverting push/pull makes distributed ML inference dramatically easier to deploy and scale
+- Part 2 takeaway: four concrete PyTorch optimizations — each addressing a different bottleneck — compound to a 15x gain
+- Both contributions generalize: applicable to any image classification workload, not just entomology
+- Antenna is open source — links on next slide
+
+> 📝 Optional: a single architectural overview diagram showing the full system (Antenna + multiple async workers) as a clean visual summary.
+
+---
+
+### Slide 15: Links and Q&A `[3 min]`
+
+- Antenna platform: [insectai.org](https://www.insectai.org/)
+- Worker + optimizations: [github.com/RolnickLab/ami-data-companion](https://github.com/RolnickLab/ami-data-companion)
+- Antenna backend (async job queue API): [github.com/RolnickLab/antenna](https://github.com/RolnickLab/antenna)
+
+> 📝 Keep this slide simple. White background, links, optional QR codes. Leave space for Q&A discussion.
+
+---
+
+## Diagram Summary
+
+All six diagrams should be created as SVG for reuse across slides and poster.
+
+| # | Name | Type | Key content |
+| --- | ------------------------ | ------------------------ | -------------------------------------------------------------------------------------- |
+| A | Push model — before | Mermaid sequence diagram | Antenna pushes to PS; PS requires public endpoint; synchronous blocking response |
+| B | Pull model — after | Mermaid sequence diagram | PS worker polls job queue; pulls batches; posts results; two workers shown for scale |
+| C | GPU utilization — before | Gantt/timeline | GPU and CPU rows; large idle gaps on GPU; reference nvidia-smi screenshot if available |
+| D | CPU-GPU data flow | Horizontal flow diagram | Before: GPU→CPU→GPU; After: GPU→GPU direct; color-code the unnecessary CPU hop |
+| E | CUDA stream pipeline | Gantt/timeline | Before: sequential transfer/compute; After: overlapping streams |
+| F | GPU utilization — after | Gantt/timeline | Mirror of Diagram C; near-continuous GPU utilization |
+
+---
+
+## Design Notes
+
+- **Style:** Clean and minimal. White or off-white background. One or two accent colors (suggestion: deep blue `#1A5276` with a light complementary tone). No decorative elements.
+- **Font:** Single sans-serif family throughout (e.g. Inter, Source Sans Pro). Body text no smaller than 24pt on slides.
+- **Diagrams:** All as SVG. Consistent line weight and color palette across all six. Must also render legibly on an A0 conference poster.
+- **Code snippets:** Monospace font with syntax highlighting. Limit to 5–8 lines per slide. Only include where they add concrete value.
+- **Key numbers:** 1,674s, 108s, 15x, 42K images, 77 min — typographically prominent wherever they appear. Consider large-text treatment on results slides.
+- **Placeholders:** Slides 2 and 13 require screenshots from the Antenna platform. These will be provided separately. Leave clearly labeled placeholder rectangles in the layout.
diff --git a/antenna/styles.css b/antenna/styles.css
new file mode 100644
index 0000000..10e5ad7
--- /dev/null
+++ b/antenna/styles.css
@@ -0,0 +1,1033 @@
+:root {
+ /* SSEC global tokens from uw-ssec.github.io/design-system */
+ --color-purple-950: #1a0038;
+ --color-purple-900: #32006e;
+ --color-purple-800: #3d1a7a;
+ --color-purple-700: #4b2e83;
+ --color-purple-600: #5a3d94;
+ --color-purple-200: #c5b4e3;
+ --color-purple-100: #e0d7f0;
+ --color-purple-50: #f0ecf7;
+
+ --color-gold-700: #85754d;
+ --color-gold-500: #b7a57a;
+ --color-gold-200: #e8e3d3;
+ --color-gold-100: #f2efe6;
+ --color-gold-50: #f9f7f2;
+
+ --color-teal-600: #1fb8b0;
+ --color-teal-500: #2ad2c9;
+ --color-teal-300: #8fe9e4;
+ --color-green-600: #2e7d32;
+ --color-green-100: #e8f5e9;
+ --color-pink-600: #c6287c;
+ --color-pink-100: #fde8f4;
+
+ --color-neutral-950: #0d0f10;
+ --color-neutral-900: #1a1d1f;
+ --color-neutral-700: #373a3c;
+ --color-neutral-600: #555a5e;
+ --color-neutral-500: #72777c;
+ --color-neutral-300: #c2c6ca;
+ --color-neutral-200: #d5d8de;
+ --color-neutral-100: #eceef0;
+ --color-neutral-50: #f6f7f8;
+ --color-white: #ffffff;
+
+ --heading-font: "Encode Sans", Helvetica, sans-serif;
+ --ui-font: "Encode Sans Compressed", "Encode Sans", Helvetica, sans-serif;
+ --body-font: "Open Sans", Helvetica, sans-serif;
+ --mono-font: "JetBrains Mono", "Fira Code", "Source Code Pro", Consolas, monospace;
+
+ --base-font-size: 32px;
+ --text-size: 15pt;
+ --h1-size: 46pt;
+ --h2-size: 29pt;
+ --h3-size: 20pt;
+ --footnote-size: 10pt;
+
+ --slide-padding: 56px;
+ --slide-padding-top: 54px;
+ --content-gap: 24px;
+ --radius-md: 8px;
+ --angle-brand: 15deg;
+
+ /* Backward-compatible aliases used by inline slide content. */
+ --background-color: var(--color-gold-50);
+ --section-divider-bg: var(--color-purple-950);
+ --primary-color: var(--color-purple-700);
+ --secondary-color: var(--color-gold-700);
+ --accent-blue: var(--color-teal-600);
+ --text-color: var(--color-neutral-900);
+ --muted-color: var(--color-neutral-600);
+ --line-color: var(--color-neutral-200);
+ --card-bg: var(--color-white);
+ --card-border: var(--color-neutral-200);
+}
+
+.reveal {
+ font-family: var(--body-font);
+ font-size: var(--base-font-size);
+ color: var(--text-color);
+}
+
+.reveal-viewport {
+ background: var(--color-purple-950);
+}
+
+.reveal strong,
+.reveal b {
+ font-weight: 700;
+}
+
+.reveal h1,
+.reveal h2,
+.reveal h3,
+.reveal h4,
+.reveal h5,
+.reveal h6 {
+ color: var(--text-color);
+ font-family: var(--heading-font);
+ font-weight: 700;
+ letter-spacing: 0;
+ line-height: 1.15;
+ text-transform: none;
+}
+
+.reveal h1 {
+ font-size: var(--h1-size);
+ font-weight: 800;
+}
+
+.reveal h2 {
+ font-size: var(--h2-size);
+ margin: 0;
+}
+
+.reveal h3 {
+ font-size: var(--h3-size);
+}
+
+.reveal p,
+.reveal li,
+.reveal td,
+.reveal th,
+.reveal blockquote {
+ color: var(--text-color);
+ font-size: var(--text-size);
+ line-height: 1.52;
+}
+
+.reveal ul {
+ list-style: none;
+ margin: 12px 0;
+ padding-left: 0;
+}
+
+.reveal ul li {
+ margin-bottom: 8px;
+ padding-left: 22px;
+ position: relative;
+}
+
+.reveal ul li::before {
+ background: var(--color-purple-700);
+ content: "";
+ height: 7px;
+ left: 0;
+ position: absolute;
+ top: 0.68em;
+ transform: skewX(-15deg);
+ width: 10px;
+}
+
+.reveal ul ul li::before {
+ background: var(--color-gold-700);
+ height: 5px;
+ width: 7px;
+}
+
+.reveal .slides section {
+ background:
+ linear-gradient(90deg, rgba(75, 46, 131, 0.06), transparent 34%),
+ var(--color-white);
+ box-sizing: border-box;
+ display: flex !important;
+ flex-direction: column !important;
+ height: 100%;
+ overflow: hidden;
+ padding: var(--slide-padding-top) var(--slide-padding) var(--slide-padding) var(--slide-padding) !important;
+ text-align: left;
+}
+
+.reveal .slides section:nth-child(4n + 2):not(.section-divider):not(.stack),
+.reveal .slides section.warm-slide {
+ background:
+ linear-gradient(105deg, rgba(183, 165, 122, 0.18), transparent 36%),
+ var(--color-gold-50);
+}
+
+.reveal .slides section.stack {
+ background: transparent;
+ padding: 0 !important;
+}
+
+.reveal .slides section:not(.stack)::before {
+ color: var(--color-gold-700);
+ content: "UW SSEC";
+ font-family: var(--ui-font);
+ font-size: 10pt;
+ font-weight: 700;
+ left: var(--slide-padding);
+ letter-spacing: 0.12em;
+ line-height: 1;
+ position: absolute;
+ text-transform: uppercase;
+ top: 24px;
+ z-index: 2;
+}
+
+.reveal .slides section:not(.section-divider):not(.stack)::after {
+ background: repeating-linear-gradient(
+ 105deg,
+ transparent,
+ transparent 9px,
+ rgba(75, 46, 131, 0.07) 9px,
+ rgba(75, 46, 131, 0.07) 10px
+ );
+ bottom: -120px;
+ content: "";
+ height: 220px;
+ pointer-events: none;
+ position: absolute;
+ right: -80px;
+ transform: rotate(var(--angle-brand));
+ width: 300px;
+}
+
+.reveal .slides section > .content {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ justify-content: flex-start;
+ min-height: 0;
+ overflow: hidden;
+ padding-top: var(--content-gap);
+ text-align: left;
+ z-index: 1;
+}
+
+.reveal .slides section > .footnote {
+ bottom: 14px;
+ color: var(--muted-color);
+ font-size: var(--footnote-size);
+ left: var(--slide-padding);
+ position: absolute;
+ right: var(--slide-padding);
+ z-index: 2;
+}
+
+.reveal .slides section > h2 {
+ border-bottom: 3px solid var(--color-gold-500);
+ color: var(--color-purple-700);
+ flex-shrink: 0;
+ padding-bottom: 12px;
+ position: relative;
+ z-index: 1;
+}
+
+.reveal .slides section > h2::before {
+ background: var(--color-purple-700);
+ content: "";
+ display: inline-block;
+ height: 24px;
+ margin-right: 12px;
+ transform: skewX(-15deg);
+ vertical-align: -3px;
+ width: 9px;
+}
+
+.reveal .slides section.section-divider {
+ align-items: flex-start !important;
+ background:
+ repeating-linear-gradient(
+ 105deg,
+ transparent,
+ transparent 10px,
+ rgba(255, 255, 255, 0.035) 10px,
+ rgba(255, 255, 255, 0.035) 11px
+ ),
+ radial-gradient(circle at 82% 26%, rgba(42, 210, 201, 0.16), transparent 25%),
+ linear-gradient(135deg, var(--color-purple-950), var(--color-purple-900));
+ color: var(--color-white);
+ justify-content: center !important;
+ text-align: left;
+}
+
+.reveal .slides section.section-divider::before {
+ color: var(--color-gold-500);
+}
+
+.reveal .slides section.section-divider::after {
+ background: var(--color-gold-500);
+ bottom: 0;
+ clip-path: polygon(0 35%, 100% 0, 100% 100%, 0 100%);
+ content: "";
+ height: 72px;
+ left: 0;
+ position: absolute;
+ right: 0;
+}
+
+.reveal .slides section.section-divider h1 {
+ color: var(--color-white);
+ font-size: 52pt;
+ letter-spacing: 0;
+ max-width: 940px;
+ text-align: left;
+ z-index: 1;
+}
+
+.reveal .slides section.section-divider h1::after {
+ background: var(--color-teal-500);
+ content: "";
+ display: block;
+ height: 4px;
+ margin: 16px 0 0;
+ transform: skewX(-15deg);
+ width: 112px;
+}
+
+.reveal .slides section.section-divider p {
+ color: var(--color-gold-200);
+ font-size: 20pt;
+ z-index: 1;
+}
+
+.reveal .slides section.section-divider .subtitle {
+ color: var(--color-gold-200);
+ font-size: 18pt;
+ margin-top: 16px;
+ max-width: 760px;
+}
+
+.reveal .slides section.section-divider .event-tag {
+ color: var(--color-teal-300);
+ font-family: var(--ui-font);
+ font-size: 12pt;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ margin-bottom: 20px;
+ text-transform: uppercase;
+}
+
+.text-lg { font-size: 18pt !important; }
+.text-xl { font-size: 20pt !important; }
+.text-2xl { font-size: 24pt !important; }
+.text-3xl { font-size: 28pt !important; }
+.text-4xl { font-size: 32pt !important; }
+.text-muted { color: var(--muted-color) !important; }
+.text-red { color: var(--color-purple-700) !important; }
+.text-green { color: var(--color-gold-700) !important; }
+.text-blue { color: var(--color-teal-600) !important; }
+.text-center { text-align: center !important; }
+.text-uppercase {
+ font-family: var(--ui-font);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+.font-mono { font-family: var(--mono-font) !important; }
+
+.reveal blockquote {
+ background: var(--color-gold-50);
+ border: 0;
+ box-shadow: inset 7px 0 0 var(--color-gold-500);
+ margin: 16px 0;
+ max-width: none;
+ padding: 18px 22px 18px 28px;
+ width: 100%;
+}
+
+.reveal blockquote p {
+ color: var(--color-neutral-900);
+ font-size: 16pt;
+ font-style: italic;
+}
+
+.reveal blockquote cite {
+ color: var(--color-neutral-600);
+ display: block;
+ font-style: normal;
+ margin-top: 8px;
+}
+
+.reveal table {
+ border-collapse: collapse;
+ box-shadow: 0 1px 3px rgba(13, 15, 16, 0.08);
+ font-size: 12pt;
+ width: 100%;
+}
+
+.reveal table th {
+ background: var(--color-purple-700);
+ border: none;
+ color: var(--color-white);
+ font-family: var(--ui-font);
+ font-size: 11pt;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ padding: 9px 12px;
+ text-align: left;
+ text-transform: uppercase;
+}
+
+.reveal table td {
+ border-bottom: 1px solid var(--color-neutral-200);
+ color: var(--color-neutral-900);
+ font-size: 12pt;
+ padding: 8px 12px;
+ vertical-align: top;
+}
+
+.reveal table tr:nth-child(even) td {
+ background: var(--color-gold-50);
+}
+
+.card,
+.card-red,
+.card-green,
+.card-blue {
+ background: var(--color-white);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: var(--radius-md);
+ box-shadow: 0 1px 3px rgba(13, 15, 16, 0.08);
+ overflow: hidden;
+ padding: 18px 20px;
+ position: relative;
+}
+
+.card::before,
+.card-red::before,
+.card-green::before,
+.card-blue::before {
+ background: var(--color-purple-700);
+ content: "";
+ height: 4px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.card-red {
+ background: var(--color-purple-50);
+ border-color: var(--color-purple-200);
+}
+
+.card-red::before {
+ background: var(--color-purple-700);
+}
+
+.card-green {
+ background: var(--color-gold-50);
+ border-color: var(--color-gold-200);
+}
+
+.card-green::before {
+ background: var(--color-gold-500);
+}
+
+.card-blue {
+ background: #effcfc;
+ border-color: var(--color-teal-300);
+}
+
+.card-blue::before {
+ background: var(--color-teal-500);
+}
+
+.reveal .badge {
+ background: var(--color-purple-700);
+ border-radius: 0;
+ color: var(--color-white);
+ display: inline-block;
+ font-family: var(--ui-font);
+ font-size: 10pt;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ margin-bottom: 8px;
+ padding: 4px 10px;
+ text-transform: uppercase;
+ transform: skewX(-15deg);
+}
+
+.reveal .badge-green {
+ background: var(--color-gold-700);
+}
+
+.reveal .badge-blue {
+ background: var(--color-teal-500);
+ color: var(--color-purple-950);
+}
+
+.check-item {
+ align-items: flex-start;
+ display: flex;
+ gap: 12px;
+ margin-bottom: 8px;
+}
+
+.check-item i {
+ color: var(--color-purple-700) !important;
+ flex-shrink: 0;
+ margin-top: 4px;
+}
+
+.reveal code {
+ background: var(--color-purple-50);
+ border: 1px solid var(--color-purple-100);
+ border-radius: 4px;
+ color: var(--color-purple-700);
+ font-family: var(--mono-font);
+ font-size: 0.86em;
+ padding: 2px 6px;
+}
+
+.section-divider code {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(42, 210, 201, 0.22);
+ color: var(--color-teal-300);
+}
+
+/* ── Antenna-specific extensions ────────────────────────────────── */
+
+/* Large stat/metric numbers */
+.stat-number {
+ font-family: var(--heading-font);
+ font-size: 56pt;
+ font-weight: 800;
+ color: var(--color-purple-700);
+ line-height: 1;
+ letter-spacing: -0.02em;
+}
+
+.stat-number-hero {
+ font-size: 72pt;
+ color: var(--color-teal-600);
+}
+
+.stat-number-before {
+ font-size: 36pt;
+ color: var(--color-purple-600);
+}
+
+.stat-number-after {
+ font-size: 36pt;
+ color: var(--color-teal-600);
+}
+
+.stat-label {
+ font-family: var(--ui-font);
+ font-size: 11pt;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted-color);
+ margin-top: 4px;
+}
+
+/* Screenshot / diagram placeholders */
+.diagram-placeholder {
+ background: var(--color-neutral-50);
+ border: 2px dashed var(--color-neutral-300);
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 160px;
+ padding: 20px;
+}
+
+.diagram-placeholder .diagram-label {
+ font-family: var(--ui-font);
+ font-size: 11pt;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--color-neutral-500);
+ font-weight: 700;
+}
+
+.screenshot-frame {
+ background: var(--color-white);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: var(--radius-md);
+ box-shadow: 0 14px 30px rgba(26, 0, 56, 0.14);
+ box-sizing: border-box;
+ display: flex;
+ height: 280px;
+ margin: 0;
+ overflow: hidden;
+ padding: 8px;
+}
+
+.screenshot-frame img {
+ border-radius: calc(var(--radius-md) - 2px);
+ display: block;
+ height: 100%;
+ object-fit: contain;
+ width: 100%;
+}
+
+/* Architecture / flow diagrams */
+.diag-flow {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ background: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: var(--radius-md);
+ padding: 14px 16px;
+}
+
+.diag-box {
+ flex-shrink: 0;
+ padding: 10px 14px;
+ border-radius: 6px;
+ font-size: 13pt;
+ font-weight: 600;
+ font-family: var(--heading-font);
+ text-align: center;
+ line-height: 1.3;
+ min-width: 90px;
+ border: 2px solid;
+}
+
+.diag-box small {
+ display: block;
+ font-size: 9pt;
+ font-weight: 400;
+ opacity: 0.75;
+}
+
+.diag-sm {
+ font-size: 12pt;
+ padding: 7px 12px;
+}
+
+.diag-primary {
+ border-color: var(--color-purple-700);
+ background: var(--color-purple-50);
+ color: var(--color-purple-700);
+}
+
+.diag-neutral {
+ border-color: var(--color-neutral-400);
+ background: var(--color-white);
+ color: var(--color-neutral-700);
+}
+
+.diag-success {
+ border-color: var(--color-teal-500);
+ background: #effcfc;
+ color: var(--color-teal-600);
+}
+
+.diag-tag {
+ display: block;
+ margin-top: 5px;
+ background: var(--color-gold-100);
+ color: var(--color-gold-700);
+ font-size: 9pt;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-weight: 700;
+ font-family: var(--ui-font);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.diag-conn {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 0 10px;
+}
+
+.diag-arrow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10pt;
+ color: var(--muted-color);
+}
+
+.diag-arrow .diag-line {
+ flex: 1;
+ height: 2px;
+ background: var(--color-neutral-400);
+ position: relative;
+}
+
+.diag-arrow.right .diag-line::after {
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: -4px;
+ border-left: 7px solid var(--color-neutral-400);
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+}
+
+.diag-arrow.left .diag-line::before {
+ content: '';
+ position: absolute;
+ left: -1px;
+ top: -4px;
+ border-right: 7px solid var(--color-neutral-400);
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+}
+
+.diag-arrow.blocking .diag-line {
+ background: var(--color-purple-400);
+}
+
+.diag-arrow.blocking .diag-line::before {
+ border-right-color: var(--color-purple-400);
+}
+
+.diag-arrow.blocking .diag-arrow-lbl {
+ color: var(--color-purple-600);
+ font-weight: 600;
+}
+
+.diag-arrow-lbl {
+ font-size: 9pt;
+ color: var(--muted-color);
+ white-space: nowrap;
+}
+
+/* CPU-GPU data flow (before/after rows) */
+.data-flow {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ background: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: var(--radius-md);
+ padding: 12px 14px;
+}
+
+.data-flow-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.df-row-label {
+ font-size: 9pt;
+ font-weight: 700;
+ font-family: var(--ui-font);
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ width: 40px;
+ flex-shrink: 0;
+ color: var(--muted-color);
+}
+
+.df-node {
+ padding: 7px 12px;
+ border-radius: 5px;
+ font-size: 11pt;
+ font-weight: 600;
+ font-family: var(--heading-font);
+ text-align: center;
+ flex-shrink: 0;
+ line-height: 1.2;
+}
+
+.df-node small {
+ display: block;
+ font-size: 9pt;
+ font-weight: 400;
+}
+
+.df-gpu {
+ background: var(--color-teal-300);
+ color: var(--color-purple-950);
+}
+
+.df-cpu {
+ background: var(--color-purple-100);
+ color: var(--color-purple-700);
+ border: 1px dashed var(--color-purple-400);
+}
+
+.df-arrow-seg {
+ flex: 1;
+ height: 2px;
+ position: relative;
+}
+
+.df-arrow-seg::after {
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: -3px;
+ border-left: 6px solid;
+ border-top: 3px solid transparent;
+ border-bottom: 3px solid transparent;
+}
+
+.df-arrow-seg.cpu-path {
+ background: var(--color-purple-400);
+}
+
+.df-arrow-seg.cpu-path::after {
+ border-left-color: var(--color-purple-400);
+}
+
+.df-arrow-seg.gpu-path {
+ background: var(--color-teal-500);
+}
+
+.df-arrow-seg.gpu-path::after {
+ border-left-color: var(--color-teal-500);
+}
+
+/* Gantt-style timelines (GPU utilization, CUDA streams) */
+.timeline-container {
+ border: 1px solid var(--color-neutral-200);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.timeline-row {
+ display: flex;
+ align-items: stretch;
+}
+
+.timeline-row + .timeline-row {
+ border-top: 1px solid var(--color-neutral-200);
+}
+
+.timeline-label {
+ width: 48px;
+ padding: 6px 8px;
+ font-size: 10pt;
+ font-weight: 700;
+ font-family: var(--ui-font);
+ color: var(--muted-color);
+ border-right: 1px solid var(--color-neutral-200);
+ flex-shrink: 0;
+ text-align: right;
+ background: var(--color-neutral-50);
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.timeline-track {
+ flex: 1;
+ height: 36px;
+ display: flex;
+}
+
+.timeline-active {
+ background: var(--color-teal-500);
+ height: 100%;
+}
+
+.timeline-active-b {
+ background: var(--color-teal-600);
+ height: 100%;
+}
+
+.timeline-cpu {
+ background: var(--color-gold-500);
+ height: 100%;
+}
+
+.timeline-idle {
+ background: var(--color-neutral-100);
+ height: 100%;
+ border-left: 1px dashed var(--color-neutral-300);
+ border-right: 1px dashed var(--color-neutral-300);
+}
+
+/* CUDA stream bars */
+.stream-timeline {
+ display: flex;
+ align-items: center;
+ gap: 0;
+}
+
+.stream-label {
+ width: 40px;
+ font-size: 9pt;
+ font-weight: 700;
+ font-family: var(--ui-font);
+ color: var(--muted-color);
+ text-align: right;
+ padding-right: 8px;
+ flex-shrink: 0;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.stream-track {
+ flex: 1;
+ height: 40px;
+ display: flex;
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.stream-bar {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11pt;
+ font-weight: 700;
+ font-family: var(--ui-font);
+ letter-spacing: 0.04em;
+}
+
+.stream-transfer {
+ background: var(--color-purple-600);
+ color: var(--color-white);
+}
+
+.stream-compute {
+ background: var(--color-teal-500);
+ color: var(--color-purple-950);
+}
+
+.stream-transfer-b {
+ background: var(--color-purple-200);
+ color: var(--color-purple-800);
+}
+
+.stream-compute-b {
+ background: var(--color-teal-300);
+ color: var(--color-purple-950);
+}
+
+.stream-gap {
+ background: var(--color-neutral-100);
+ height: 100%;
+}
+
+/* Code blocks */
+pre.code-block {
+ background: var(--color-neutral-950);
+ border-radius: var(--radius-md);
+ padding: 14px 18px;
+ margin: 0;
+ overflow: auto;
+}
+
+pre.code-block code {
+ background: transparent;
+ border: none;
+ color: var(--color-teal-300);
+ font-family: var(--mono-font);
+ font-size: 13pt;
+ padding: 0;
+ white-space: pre;
+ line-height: 1.5;
+}
+
+/* Diagram section label */
+.diag-label {
+ font-family: var(--ui-font);
+ font-size: 10pt;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted-color);
+ margin-bottom: 8px;
+}
+
+/* Full-width SVG diagram images (from UXDesigner SVG assets) */
+.diagram-full {
+ display: block;
+ inline-size: 100%;
+ max-block-size: 55vh;
+ min-height: 0;
+ object-fit: contain;
+}
+
+/* Let the batch data-loader iframe use the full slide content area. */
+#batch-data-loader .diagram-full {
+ block-size: 100%;
+ max-block-size: none;
+}
+
+#fix1-batches .diagram-full {
+ block-size: 100%;
+ max-block-size: none;
+}
+
+#batch-data-loader .content,
+#baseline .content {
+ padding-top: 12px;
+}
+
+#baseline .stat-banner {
+ margin-bottom: 6px;
+ padding: 4px 0 8px;
+}
+
+#baseline .diagram-full {
+ max-block-size: none;
+}
+
+/* Intro sentence above full-width visual (pattern A / C slides) */
+.slide-intro {
+ color: var(--color-neutral-700);
+ font-size: 15pt;
+ font-weight: 600;
+ line-height: 1.4;
+ margin-bottom: 14px;
+ max-width: 960px;
+}
+
+/* Stat banner strip (pattern B — Slides 9 and 14) */
+.stat-banner {
+ align-items: baseline;
+ border-bottom: 1px solid var(--color-neutral-200);
+ display: flex;
+ gap: 16px;
+ margin-bottom: 14px;
+ padding: 6px 0 12px;
+}
+
+.stat-banner .stat-number {
+ font-size: 40pt;
+}
+
+.stat-banner-desc {
+ color: var(--muted-color);
+ font-family: var(--ui-font);
+ font-size: 13pt;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+}
+
+/* Full-height screenshot frame variant (Slide 2) */
+.screenshot-frame.screenshot-flex {
+ flex: 1;
+ height: auto;
+}
+
+@media print {
+ .reveal .slides section:not(.stack)::before {
+ print-color-adjust: exact;
+ }
+}
diff --git a/index.html b/index.html
index a08735e..4739ead 100644
--- a/index.html
+++ b/index.html
@@ -152,7 +152,7 @@
transition: box-shadow 0.15s ease, transform 0.15s ease;
}
- .card:hover {
+ .card:not(.card--coming-soon):hover {
box-shadow: 0 4px 20px rgba(75, 46, 131, 0.13);
transform: translateY(-2px);
}
@@ -192,6 +192,19 @@
text-transform: uppercase;
}
+ .status-badge {
+ background: var(--color-neutral-200);
+ border: 1px solid var(--color-neutral-600);
+ border-radius: 4px;
+ color: var(--color-neutral-900);
+ font-family: var(--ui-font);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.07em;
+ padding: 2px 8px;
+ text-transform: uppercase;
+ }
+
.card-title {
font-family: var(--heading-font);
font-size: 20px;
@@ -246,6 +259,38 @@
display: inline;
}
+ .card-status {
+ color: var(--color-neutral-700);
+ font-family: var(--heading-font);
+ font-size: 13px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ }
+
+ .card--coming-soon {
+ background: var(--color-gold-100);
+ border-color: var(--color-neutral-200);
+ box-shadow: none;
+ }
+
+ .card--coming-soon::before {
+ background: var(--color-neutral-600);
+ }
+
+ .card--coming-soon .tag {
+ background: var(--color-white);
+ color: var(--color-neutral-700);
+ }
+
+ .card--coming-soon .card-title {
+ color: var(--color-neutral-700);
+ }
+
+ .card--coming-soon .card-footer {
+ background: var(--color-gold-100);
+ border-top-color: var(--color-neutral-200);
+ }
+
/* ── Footer ────────────────────────────────────── */
.site-footer {
border-top: 1px solid var(--color-neutral-200);
@@ -285,19 +330,20 @@ Presentations
-
Available presentations
+
Presentation gallery
-
+
Security
Supply Chain
AI
+ Coming soon
Open Source Supply Chain Security: Threats, Mitigations & Hardened Workflows
@@ -305,7 +351,7 @@
UW SSEC RSE Meetup — May 14, 2026