Spaces:
Paused
Paused
Download pff_pk/inference/protocol_validation.py from Dariusfar/pff-pk-api: direct link, hf CLI and curl.
- Browser
- Download file 9.64 kB
-
https://huggingface.co/spaces/Dariusfar/pff-pk-api/resolve/main/pff_pk/inference/protocol_validation.py
- Command line
-
hf download hf://spaces/Dariusfar/pff-pk-api/pff_pk/inference/protocol_validation.py
-
curl -L -o protocol_validation.py https://huggingface.co/spaces/Dariusfar/pff-pk-api/resolve/main/pff_pk/inference/protocol_validation.py
9.64 kB
| """Fixed-stratum generation and plotting for v6 protocol validation.""" | |
| from __future__ import annotations | |
| import hashlib | |
| from dataclasses import fields, replace | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import torch | |
| from pff_pk.tasks.batch import FlowBatch | |
| from pff_pk.tasks.batching import inverse_concentration | |
| CONTEXT_COLOR = "#ff7f0e" | |
| GENERATED_COLOR = "#1f77b4" | |
| TRUTH_COLOR = "#202020" | |
| PROTOCOL_CATEGORIES = { | |
| "identity": ( | |
| "reference_identity", | |
| "global_identity", | |
| "add_on_identity", | |
| ), | |
| "dose_scaling": ( | |
| "reference_to_lower_scale", | |
| "reference_to_higher_scale", | |
| "lower_scale_to_reference", | |
| "higher_scale_to_reference", | |
| "global_scale_cross", | |
| ), | |
| "add_on_dose": ( | |
| "reference_to_early_add_on", | |
| "reference_to_late_add_on", | |
| "add_on_to_reference", | |
| "add_on_cross", | |
| ), | |
| "cross_intervention": ( | |
| "global_to_add_on", | |
| "add_on_to_global", | |
| ), | |
| } | |
| ACQUISITIONS = ("unscheduled", "exact", "pseudo_scheduled") | |
| def select_representatives(bank: dict) -> list[tuple[str, int, int]]: | |
| """Select one deterministic bank row for every protocol/acquisition stratum.""" | |
| candidates: dict[str, list[tuple[int, int]]] = {} | |
| for record_index, record in enumerate(bank["batches"]): | |
| candidates.setdefault(record["stratum"], []).extend( | |
| (record_index, row) for row in range(len(record["study_id"])) | |
| ) | |
| expected = { | |
| f"{case}__{acquisition}" | |
| for cases in PROTOCOL_CATEGORIES.values() | |
| for case in cases | |
| for acquisition in ACQUISITIONS | |
| } | |
| if set(candidates) != expected: | |
| raise ValueError("validation bank does not contain the expected 42 v6 strata") | |
| selected = [] | |
| for stratum in sorted(candidates): | |
| digest = hashlib.blake2b(stratum.encode(), digest_size=8).digest() | |
| choices = candidates[stratum] | |
| record_index, row = choices[int.from_bytes(digest, "little") % len(choices)] | |
| selected.append((stratum, record_index, row)) | |
| return selected | |
| def protocol_seed(stratum: str) -> int: | |
| """Return the fixed source-process seed for a protocol stratum.""" | |
| digest = hashlib.blake2b(stratum.encode(), digest_size=4).digest() | |
| return 2026 + int.from_bytes(digest, "little") | |
| def _select(batch: FlowBatch, row: int, device: torch.device) -> FlowBatch: | |
| index = torch.tensor([row], dtype=torch.long) | |
| values = {} | |
| for field in fields(batch): | |
| value = getattr(batch, field.name) | |
| if value is None: | |
| values[field.name] = None | |
| elif isinstance(value, torch.Tensor): | |
| values[field.name] = value.index_select(0, index).to(device) | |
| else: | |
| values[field.name] = [value[row]] | |
| return replace(batch, **values) | |
| def _event_arrays(batch: FlowBatch) -> tuple[np.ndarray, np.ndarray]: | |
| assert batch.context_protocol_events is not None | |
| assert batch.context_protocol_event_mask is not None | |
| assert batch.target_protocol_events is not None | |
| assert batch.target_protocol_event_mask is not None | |
| context_mask = batch.context_protocol_event_mask[0, 0].cpu().numpy() | |
| target_mask = batch.target_protocol_event_mask[0].cpu().numpy() | |
| context = batch.context_protocol_events[0, 0].cpu().numpy()[context_mask] | |
| target = batch.target_protocol_events[0].cpu().numpy()[target_mask] | |
| return context, target | |
| def generate_protocol_draws( | |
| model, cpu_batch: FlowBatch, row: int, samples: int, seed: int | |
| ) -> dict: | |
| """Generate persistent conditional draws for one fixed bank row.""" | |
| device = next(model.parameters()).device | |
| batch = _select(cpu_batch, row, device) | |
| repeated_values = {} | |
| for field in fields(batch): | |
| value = getattr(batch, field.name) | |
| if value is None: | |
| repeated_values[field.name] = None | |
| elif isinstance(value, torch.Tensor): | |
| repeated_values[field.name] = value.expand(samples, *value.shape[1:]).clone() | |
| else: | |
| repeated_values[field.name] = value * samples | |
| repeated = replace(batch, **repeated_values) | |
| cuda_devices = [device.index] if device.type == "cuda" else [] | |
| with torch.random.fork_rng(devices=cuda_devices), torch.inference_mode(): | |
| torch.manual_seed(seed) | |
| generated = inverse_concentration(model.sample(repeated), repeated)[..., 0] | |
| context = inverse_concentration(batch.context_value, batch)[0, ..., 0] | |
| target = inverse_concentration(batch.target_value, batch)[0, ..., 0] | |
| context_events, target_events = _event_arrays(batch) | |
| return { | |
| "study_id": batch.study_id[0], | |
| "context_time": batch.context_time[0, ..., 0].cpu(), | |
| "context_value": context.cpu(), | |
| "context_mask": batch.context_mask[0].cpu(), | |
| "target_time": batch.target_time[0, :, 0].cpu(), | |
| "target_value": target.cpu(), | |
| "target_mask": batch.target_mask[0].cpu(), | |
| "generated": generated.cpu(), | |
| "context_events": torch.from_numpy(context_events), | |
| "target_events": torch.from_numpy(target_events), | |
| } | |
| def _schedule(events: torch.Tensor) -> str: | |
| labels = [] | |
| for time, amount, duration, oral in events.tolist(): | |
| route = "PO" if oral >= 0.5 else "IV" | |
| label = f"{route} {amount:.3g}× @ {time:.3g}" | |
| if duration > 0: | |
| label += f" over {duration:.3g}" | |
| labels.append(label) | |
| return " + ".join(labels) if labels else "no dose" | |
| def _human_case(value: str) -> str: | |
| return value.replace("_", " ").replace("to", "→") | |
| def plot_protocol_category( | |
| records: dict[str, dict], | |
| category: str, | |
| cases: tuple[str, ...], | |
| output: Path, | |
| step: int, | |
| ) -> tuple[Path, Path]: | |
| """Render one protocol category over the three acquisition designs.""" | |
| figure, axes = plt.subplots( | |
| len(cases), | |
| len(ACQUISITIONS), | |
| figsize=(15.6, 3.25 * len(cases) + 1.4), | |
| squeeze=False, | |
| constrained_layout=True, | |
| ) | |
| legend_handles = None | |
| for row, case in enumerate(cases): | |
| for column, acquisition in enumerate(ACQUISITIONS): | |
| axis = axes[row, column] | |
| record = records[f"{case}__{acquisition}"] | |
| context_time = record["context_time"].numpy() | |
| context_value = record["context_value"].numpy() | |
| context_mask = record["context_mask"].numpy() | |
| for times, values, mask in zip( | |
| context_time, context_value, context_mask, strict=True | |
| ): | |
| axis.plot( | |
| times[mask], | |
| values[mask], | |
| color=CONTEXT_COLOR, | |
| linewidth=0.5, | |
| alpha=0.28, | |
| zorder=1, | |
| ) | |
| target_mask = record["target_mask"].numpy() | |
| target_time = record["target_time"].numpy()[target_mask] | |
| for generated in record["generated"].numpy(): | |
| axis.plot( | |
| target_time, | |
| generated[target_mask], | |
| color=GENERATED_COLOR, | |
| linewidth=1.0, | |
| alpha=0.72, | |
| zorder=3, | |
| ) | |
| truth = axis.plot( | |
| target_time, | |
| record["target_value"].numpy()[target_mask], | |
| color=TRUTH_COLOR, | |
| linestyle="--", | |
| linewidth=1.5, | |
| zorder=4, | |
| label="held-out truth", | |
| )[0] | |
| context_line = axis.plot([], [], color=CONTEXT_COLOR, label="context cohort")[0] | |
| generated_line = axis.plot([], [], color=GENERATED_COLOR, label="generated draws")[0] | |
| legend_handles = (context_line, generated_line, truth) | |
| axis.text( | |
| 0.02, | |
| 0.97, | |
| f"study {record['study_id'][-8:]}\n" | |
| f"context: {_schedule(record['context_events'])}\n" | |
| f"target: {_schedule(record['target_events'])}", | |
| transform=axis.transAxes, | |
| fontsize=7.2, | |
| ha="left", | |
| va="top", | |
| bbox={"facecolor": "white", "alpha": 0.78, "edgecolor": "none", "pad": 2}, | |
| zorder=10, | |
| ) | |
| if row == 0: | |
| axis.set_title( | |
| acquisition.replace("_", " "), | |
| fontsize=10.5, | |
| fontweight="bold", | |
| pad=8, | |
| ) | |
| if column == 0: | |
| axis.set_ylabel(f"{_human_case(case)}\nC / Cref", fontsize=8) | |
| if row == len(cases) - 1: | |
| axis.set_xlabel("dimensionless time tau", fontsize=8) | |
| axis.set_yscale("log") | |
| axis.tick_params(labelsize=7) | |
| axis.grid(alpha=0.13, linewidth=0.4) | |
| figure.suptitle( | |
| f"v6 validation: {category.replace('_', ' ')} | checkpoint step {step}\n" | |
| "one held-out family per protocol/acquisition stratum; five conditional draws", | |
| fontsize=13, | |
| ) | |
| if legend_handles is not None: | |
| figure.legend( | |
| legend_handles, | |
| [handle.get_label() for handle in legend_handles], | |
| loc="outside lower center", | |
| ncol=3, | |
| frameon=False, | |
| ) | |
| png = output / f"protocol_validation_{category}_step{step:06d}.png" | |
| pdf = png.with_suffix(".pdf") | |
| figure.savefig(png, dpi=220, facecolor="white") | |
| figure.savefig(pdf, facecolor="white") | |
| plt.close(figure) | |
| return png, pdf | |