-
Notifications
You must be signed in to change notification settings - Fork 13
Rescale and LOT correction #424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Soorya19Pradeep
wants to merge
89
commits into
main
Choose a base branch
from
rescale-lot-correction
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
89 commits
Select commit
Hold shift + click to select a range
0b529dd
Port scale-aware inference and LOT batch correction to modular-viscy-…
Soorya19Pradeep 5e6407a
Add visualization script for TripletDataModule scale-aware rescaling
Soorya19Pradeep 378a4ac
fix(viscy-data): handle nested timepoint_statistics in _collate_norm_…
edyoshikun 19a3301
feat(viscy-data): add Z-reduction and focus-centered z_range to Tripl…
edyoshikun 365fc42
docs(dynaclr): triplet inference DAG + sample 2D-from-3D predict config
edyoshikun dc39073
docs(dynaclr): add triplet z-projection dataloader inspection script
edyoshikun a14a0ac
Potential fix for pull request finding
edyoshikun 52c72a1
Potential fix for pull request finding
edyoshikun 6d320ae
Potential fix for pull request finding
edyoshikun 2a1b327
Potential fix for pull request finding
edyoshikun dbe22f2
Potential fix for pull request finding
edyoshikun 4a2de14
Potential fix for pull request finding
edyoshikun 5b81c2c
fix(viscy-data): address triplet z-reduction/rescaling review findings
edyoshikun c7f8a16
CLI to split ann data by a column to group by
edyoshikun b39205f
perf(viscy-utils): GPU-accelerate MMD via PyTorch
Soorya19Pradeep f85cc7e
feat(viscy-utils): add MMD witness function
Soorya19Pradeep fa09832
feat(dynaclr): add MMD-witness weak-label source for linear classifiers
edyoshikun c9d67ec
test(dynaclr): cover MMD-witness weak labeling for linear classifiers
edyoshikun c643e95
config(dynaclr): add witness-labeled LC recipe for infectomics
edyoshikun bfc1936
docs(dynaclr): witness-score LC DAG with flow diagrams and mock plots
edyoshikun 361a8a5
fix(dynaclr): evaluate witness LC against ground-truth annotations
edyoshikun 1d8bffa
config(dynaclr): add eval_against to witness LC recipe
edyoshikun 1af4f03
docs(dynaclr): reflect annotation-based eval in witness DAG + visuals
edyoshikun faf741d
feat(dynaclr): add witness gating diagnostic page to LC summary PDF
edyoshikun 0a17c20
feat(dynaclr): filter-based witness references + annotation-scored eval
edyoshikun b6015a9
fix(dynaclr): witness ROC/F1 plots use annotation-scored eval, not wi…
edyoshikun 401a38a
feat(dynaclr): per-marker eval target for witness classifiers
edyoshikun b769bc0
feat(dynaclr): witness-GMM annotations replace the witness label source
edyoshikun d01e749
chore(dynaclr): untrack mock DAG visuals
edyoshikun 68f3543
refactor(mmd): debloat MMD utilities
edyoshikun a3d7a5f
fix(triplet): reject non-positive pixel size in rescaling
edyoshikun 9b0a114
fix(data): clear error when batch mixes FOV timepoint stats
edyoshikun 4c6aa9f
fix(triplet): pre-resolve timepoint_statistics norm_meta to sample t
edyoshikun 4a28b20
refactor(triplet): lazy-import viscy_transforms in rescale/z_reduction
edyoshikun 7fa02de
feat(dynaclr): per-marker embedding-consistency QC (MMD + correlation…
edyoshikun 10bea7b
docs(dynaclr): gate-free witness reference default + rationale
edyoshikun 6306aa3
chore(ruff): ignore D and E501 under scripts/
edyoshikun 06f6655
feat(dynaclr): occlusion-attribution report script for classifier eval
edyoshikun 8549598
feat(dynaclr): add clustering PoP eval configs (infection/remodel/vel…
edyoshikun 5b18944
feat(dynaclr): add embedding clustering proof-of-principle scripts
edyoshikun 7cf6674
feat(dynaclr): MMD significance gate before witness-GMM labeling
edyoshikun f86b9e5
feat(dynaclr): add canonical per-marker prediction paths
edyoshikun b28e38a
feat(dynaclr): evaluate reusable dataset-scoped embeddings
edyoshikun 85374c6
feat(dynaclr): add train-predict-eval model matrix launcher
edyoshikun cd03f60
config(dynaclr): add organelle-box remodeling matrix
edyoshikun 1650293
feat(data): support parquet focus planes and cell-level splits
edyoshikun 576c124
config(dynaclr): add Zuben gut preparation and training recipes
edyoshikun 8192d75
docs(dynaclr): document run-once eval-many pipeline
edyoshikun 111af49
feat(dynaclr): Benjamini-Yekutieli FDR control for the MMD significan…
edyoshikun 87763f9
feat(viscy-utils): model-agnostic visualization module (occlusion + P…
edyoshikun 1ac6b49
feat(dynaclr): expose occluder fill in occlusion report CONFIG
edyoshikun 32bf4f0
Revert "config(dynaclr): add Zuben gut preparation and training recipes"
edyoshikun a405e30
Revert "feat(data): support parquet focus planes and cell-level splits"
edyoshikun 6ac217e
qc for the embeddings via pearson and mmd
edyoshikun 1897b8f
plotting for mmd
edyoshikun 214b398
markdown cleanup
edyoshikun 0396452
cleanp the markdowns dags
edyoshikun 55b559d
feat(dynaclr): witness-GMM label provenance + diagnostic hardening
edyoshikun 7fe82c1
remove eval matrix from gitignore
edyoshikun 7498c50
feat(dynaclr): time-matched MMD-vs-HPI diagnostic for witness-GMM
edyoshikun f6018f9
feat(dynaclr): control-anchored gate for weak-channel witness labels
edyoshikun a63d374
fix(dynaclr): make LOT MMD comparisons consistent
edyoshikun 13bb401
fix(dynaclr): repeat marker options in batch prediction
edyoshikun c854754
fix(dynaclr): forward matrix prediction settings
edyoshikun 7f3ceca
fix(viscy-data): resize rescaled patches exactly
edyoshikun b9952de
fix(dynaclr): make LOT output replacement safe
edyoshikun 89c17a0
refactor(viscy-utils): unify LC train/val split behind one helper
edyoshikun eb970dc
feat(dynaclr): witness-GMM phase-classifier configs + driver
edyoshikun 5ff2c28
feat(dynaclr): witness-GMM diagnostic plots (AUROC, prob grids, %-rem…
edyoshikun 7b03e1b
fix(data): normalize MIP depth across z sampling
edyoshikun d89f9ed
feat(dynaclr): nest embedding zarrs under {ckpt}/embeddings/
edyoshikun 37e3312
feat(dynaclr): predict-triplet foundation models, focus-centered Z, o…
edyoshikun 1fd4def
feat(dynaclr): matrix foundation rows + skip-existing progressive col…
edyoshikun 1c6455d
feat(dynaclr): organelle_remodeling collection + DynaCLR/MorphEm matrix
edyoshikun f676a9a
docs(dynaclr): un-defer 04_21 in organelle_remodeling collection desc…
edyoshikun 655a18f
feat(qc): audit_focus_slice — flag suspect focus detections (Z-aware)
edyoshikun 3f2a500
fix(dynaclr): iter_embeddings glob missing embeddings/ segment
edyoshikun 3ec6934
feat(dynaclr): embedding-consistency-qc v1/v2 split, Fréchet, HPI poo…
edyoshikun 7aa2c3d
feat(dynaclr): microscope field + mock→uninfected on organelle collec…
edyoshikun e3fd8f7
fix(dynaclr): fixed Pearson-r heatmap scale (vmin=0.8) across markers
edyoshikun d3af40e
feat(viscy-utils): per-plate HPI-binned control-reference normalization
edyoshikun 13d0547
docs(viscy-utils): when to use control-reference normalization
edyoshikun 8ea80b7
feat(dynaclr): optional control-reference normalization in linear cla…
edyoshikun 79a2634
Revert "docs(viscy-utils): when to use control-reference normalization"
edyoshikun f719022
feat(dynaclr): expand organelle remodeling matrix
edyoshikun 2abcc52
feat(dynaclr): normalize biological MMD representations
edyoshikun 2150116
feat(dynaclr): persist normalized PCA representations
edyoshikun c7c889d
docs(dynaclr): clarify pooled PCA balancing
edyoshikun d719a66
feat(dynaclr): promote pooled witness TCN stack
edyoshikun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
81 changes: 81 additions & 0 deletions
81
applications/dynaclr/configs/prediction/predict_triplet_2d_from_3d.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Sample triplet predict config: feed a 3D OME-Zarr to a 2D model. | ||
| # | ||
| # Demonstrates the TripletDataModule options that avoid materializing a separate | ||
| # 2D MIP dataset: | ||
| # - z_reduction: collapse the extracted z_range window to a single slice | ||
| # (label-free channels -> center slice, others -> max projection). | ||
| # - reference_pixel_size: rescale each patch to the model's training pixel size | ||
| # when the inference dataset was acquired at a different magnification. | ||
| # | ||
| # See docs/DAGs/inference_triplet.md for the full pipeline. | ||
| # | ||
| # TODO: point to the path to save the embeddings | ||
| # TODO: point to the path to the data | ||
| # TODO: point to the path to the tracks | ||
| # TODO: point to the path to the checkpoint | ||
|
|
||
| seed_everything: 42 | ||
| trainer: | ||
| accelerator: gpu | ||
| strategy: auto | ||
| devices: 1 | ||
| num_nodes: 1 | ||
| precision: 32-true | ||
| inference_mode: true | ||
| logger: false | ||
| callbacks: | ||
| - class_path: lightning.pytorch.callbacks.TQDMProgressBar | ||
| init_args: | ||
| refresh_rate: 10 | ||
| - class_path: viscy_utils.callbacks.embedding_writer.EmbeddingWriter | ||
| init_args: | ||
| output_path: #TODO point to the path to save the embeddings (e.g. /embeddings/embeddings.zarr) | ||
| embedding_key: features # "projections" for frozen-backbone MLP heads | ||
| overwrite: true | ||
| pca_kwargs: null # reductions left to a later step (dynaclr reduce-dimensionality) | ||
| phate_kwargs: null | ||
| umap_kwargs: null | ||
| model: | ||
| class_path: dynaclr.engine.ContrastiveModule | ||
| init_args: | ||
| encoder: | ||
| class_path: viscy_models.contrastive.ContrastiveEncoder | ||
| init_args: | ||
| backbone: convnext_tiny | ||
| in_channels: 1 | ||
| in_stack_depth: 1 # 2D model — pairs with data.z_reduction below | ||
| stem_kernel_size: [1, 4, 4] | ||
| stem_stride: [1, 4, 4] | ||
| embedding_dim: 768 | ||
| projection_dim: 32 | ||
| drop_path_rate: 0.0 # stochastic depth off at inference | ||
| example_input_array_shape: [1, 1, 1, 160, 160] | ||
| data: | ||
| class_path: viscy_data.triplet.TripletDataModule | ||
| init_args: | ||
| data_path: #TODO point to the path to the data (e.g. /registered_test.zarr) | ||
| tracks_path: #TODO point to the path to the tracks (e.g. /track_test.zarr) | ||
| source_channel: | ||
| - Phase3D | ||
| z_range: [15, 45] # 3D window; collapsed to 1 slice by z_reduction. | ||
| # Center it on the focus plane to control which planes collapse. | ||
| z_reduction: mip # "mip" (max projection) or "center" (center slice). | ||
| # Label-free channels always take the center slice; | ||
| # others are max-projected. This sets the fallback only. | ||
| reference_pixel_size: 0.1494 # µm/px of the model's TRAINING dataset. Remove (or set | ||
| # null) when the inference dataset is at the same resolution. | ||
| initial_yx_patch_size: [160, 160] # ignored when reference_pixel_size is set (computed from ratio) | ||
| final_yx_patch_size: [160, 160] # patch size fed to the model after rescale + z_reduction | ||
| batch_size: 32 | ||
| num_workers: 0 # REQUIRED for predict (avoids zarr-fork deadlock) | ||
| normalizations: | ||
| - class_path: viscy_transforms.NormalizeSampled | ||
| init_args: | ||
| keys: [Phase3D] | ||
| level: fov_statistics | ||
| subtrahend: mean | ||
| divisor: std | ||
| # augmentations omitted: predict must be deterministic. The datamodule still | ||
| # applies normalizations + reference_pixel_size rescale + z_reduction at predict time. | ||
| return_predictions: false | ||
| ckpt_path: #TODO point to the path to the checkpoint (e.g. /checkpoints/epoch=94-step=2375.ckpt) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| # Inference DAG (Triplet path) | ||
|
|
||
| Embedding inference for a trained DynaCLR encoder using `TripletDataModule` — | ||
| the **zarr + tracking** path (no parquet). Use this when you want to run a | ||
| trained checkpoint directly over an OME-Zarr store and its `ultrack` tracking, | ||
| rather than the parquet-first `MultiExperimentDataModule` path | ||
| (see [evaluation.md](evaluation.md) for the parquet path). | ||
|
|
||
| The triplet path is the one that carries the patch-rescaling | ||
| (`reference_pixel_size`) and on-the-fly Z-reduction (`z_reduction`) options, so | ||
| a 3D zarr can feed a 2D model without materializing a separate MIP dataset. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - A trained checkpoint (`last.ckpt` or a selected epoch) for a | ||
| `dynaclr.engine.ContrastiveModule`. | ||
| - The inference dataset as an OME-Zarr store with `normalization` metadata in | ||
| the FOV `zattrs` (so `NormalizeSampled` has per-FOV stats), plus a tracking | ||
| zarr/CSV directory with `track_id, t, y, x` columns. | ||
| - The model's training pixel size (µm/px) if the inference dataset was acquired | ||
| at a different magnification — passed as `reference_pixel_size` to rescale | ||
| each patch to the physical area the model was trained on. | ||
|
|
||
| ## Step-by-step detail | ||
|
|
||
| ``` | ||
| dataset.zarr (preprocessed: normalization in FOV zattrs) | ||
| tracking.zarr/CSV (track_id, t, y, x per cell) | ||
| checkpoint.ckpt (trained ContrastiveModule) | ||
| │ | ||
| ├──► predict config (TripletDataModule + ContrastiveModule + EmbeddingWriter) | ||
| ▼ | ||
| viscy predict --config configs/prediction/predict_triplet.yml | ||
| │ TripletDataModule(fit=False): samples ONE anchor patch per (cell, timepoint) | ||
| │ - extracts z_range window, yx at initial_yx_patch_size | ||
| │ - reference_pixel_size → extract larger patch, BatchedZoomd to final_yx | ||
| │ - z_reduction → BatchedChannelWiseZReductiond collapses Z to 1 (2D model) | ||
| │ ContrastiveModule.predict_step → backbone features (+ projections) | ||
| │ EmbeddingWriter accumulates (features, index) and writes one combined store | ||
| ▼ | ||
| embeddings.zarr (AnnData: obsm["X_backbone"], obs = fov_name/track_id/t/...) | ||
| │ | ||
| ▼ | ||
| dynaclr split-embeddings --input embeddings.zarr --output-dir embeddings/ | ||
| │ groups rows by obs["experiment"], writes one zarr per experiment | ||
| │ removes the combined store afterwards | ||
| ▼ | ||
| embeddings/{experiment}.zarr (one per experiment, informatively named) | ||
| │ | ||
| ▼ | ||
| downstream eval (reduce-dimensionality, linear classifiers, MMD, pseudotime …) | ||
| see evaluation.md / pseudotime.md | ||
| ``` | ||
|
|
||
| ## Pipeline DAG (process dependency) | ||
|
|
||
| ``` | ||
| predict config + checkpoint + zarr + tracking | ||
| │ | ||
| ▼ | ||
| viscy predict (GPU, minutes–hours by cell count) | ||
| │ | ||
| ▼ | ||
| split-embeddings (CPU, ~1 min, I/O bound) | ||
| │ | ||
| ▼ | ||
| downstream eval (CPU/GPU, per analysis) | ||
| ``` | ||
|
|
||
| ## Key commands | ||
|
|
||
| | Step | Command | Input | Output | | ||
| | ---------------- | --------------------------------------------------------------------------- | --------------------------------------- | ----------------------------------- | | ||
| | Predict | `uv run viscy predict --config configs/prediction/predict_triplet.yml` | predict config + ckpt + zarr + tracking | combined `embeddings.zarr` | | ||
| | Predict (SLURM) | `sbatch configs/prediction/predict_triplet.sh` | same | combined `embeddings.zarr` | | ||
| | Split embeddings | `dynaclr split-embeddings --input embeddings.zarr --output-dir embeddings/` | combined zarr with `obs["experiment"]` | one `{experiment}.zarr` per dataset | | ||
|
|
||
| ## What lives where | ||
|
|
||
| | Data | Location | When written | | ||
| | --------------------------------- | ------------------------------------------------- | --------------------- | | ||
| | Pixel data (TCZYX) | dataset.zarr on VAST | data prep | | ||
| | Cell tracks (track_id, t, y, x) | tracking.zarr / CSV on VAST | data prep | | ||
| | Normalization stats (per FOV) | dataset.zarr FOV `zattrs["normalization"]` | `viscy preprocess` | | ||
| | Backbone embeddings | `embeddings.zarr` → `obsm["X_backbone"]` | `viscy predict` | | ||
| | Cell index (fov_name/track_id/t) | `embeddings.zarr` → `obs` | `viscy predict` | | ||
| | Per-experiment embeddings | `embeddings/{experiment}.zarr` | `split-embeddings` | | ||
|
|
||
| ## Predict config structure | ||
|
|
||
| A ready-to-edit sample lives at | ||
| [`configs/prediction/predict_triplet_2d_from_3d.yml`](../../configs/prediction/predict_triplet_2d_from_3d.yml) | ||
| (the 2D-from-3D case, with `z_reduction` + `reference_pixel_size`). The skeleton | ||
| below annotates the load-bearing fields: | ||
|
|
||
| ```yaml | ||
| seed_everything: 42 | ||
|
|
||
| trainer: | ||
| accelerator: gpu | ||
| devices: 1 | ||
| precision: 32-true | ||
| inference_mode: true | ||
| logger: false | ||
| callbacks: | ||
| - class_path: viscy_utils.callbacks.embedding_writer.EmbeddingWriter | ||
| init_args: | ||
| output_path: /path/to/embeddings/embeddings.zarr | ||
| embedding_key: features # "projections" for frozen-backbone MLP heads | ||
| overwrite: true | ||
|
|
||
| model: | ||
| class_path: dynaclr.engine.ContrastiveModule | ||
| init_args: | ||
| encoder: | ||
| class_path: viscy_models.contrastive.ContrastiveEncoder | ||
| init_args: | ||
| backbone: convnext_tiny | ||
| in_channels: 1 | ||
| in_stack_depth: 1 # 2D model — pair with z_reduction below | ||
| # … must match the trained checkpoint's encoder args … | ||
|
|
||
| data: | ||
| class_path: viscy_data.TripletDataModule | ||
| init_args: | ||
| data_path: /path/to/dataset.zarr | ||
| tracks_path: /path/to/tracking.zarr | ||
| source_channel: [Phase3D] | ||
| z_range: [0, 16] # window collapsed by z_reduction | ||
| final_yx_patch_size: [160, 160] | ||
| reference_pixel_size: 0.1494 # rescale to the model's training pixel size (optional) | ||
| z_reduction: mip # collapse z_range to 1 slice for a 2D model (optional) | ||
| batch_size: 400 | ||
| num_workers: 0 # REQUIRED for predict (see Notes) | ||
| predict_cells: false # true + include_fov_names/include_track_ids to subset | ||
| normalizations: | ||
| - class_path: viscy_transforms.NormalizeSampled | ||
| init_args: | ||
| keys: [Phase3D] | ||
| subtrahend: mean | ||
| divisor: std | ||
| augmentations: [] # MUST be empty for deterministic predict | ||
|
|
||
| ckpt_path: /path/to/checkpoint/last.ckpt | ||
| return_predictions: false # writer persists to zarr; don't hold in memory | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - **`num_workers: 0` is required for the predict path.** `HCSDataModule`/ | ||
| `TripletDataModule` predict does not use `mmap_preload`, and >0 workers risks a | ||
| zarr-fork deadlock. This matches the dynacell predict overlay. | ||
| - **`augmentations: []`** — predict must be deterministic. The datamodule still | ||
| applies `normalizations` (and the `reference_pixel_size` rescale + `z_reduction` | ||
| collapse) at predict time via `_no_augmentation_transform`; only random | ||
| augmentations are dropped. | ||
| - **2D from 3D without a MIP dataset.** Set `z_reduction: mip` (or `center`) to | ||
| collapse the extracted `z_range` window to a single slice. Label-free channels | ||
| (resolved by name via `parse_channel_name`) take the center slice; all other | ||
| channels are max-projected. Pair with `in_stack_depth: 1` on the encoder. | ||
| Center the `z_range` on the focus plane to control which planes are collapsed. | ||
| - **Pixel-size rescaling.** When the inference dataset's pixel size differs from | ||
| the model's training pixel size, set `reference_pixel_size` (µm/px) so a larger | ||
| patch covering the same physical area is extracted and bilinearly resized to | ||
| `final_yx_patch_size`. Leave unset for same-resolution datasets. | ||
| - **`embedding_key`.** Use `features` for the backbone output (most models) and | ||
| `projections` for frozen-backbone MLP-head models, which writes | ||
| `obsm["X_projections"]` instead. | ||
| - **`split-embeddings` requires `obs["experiment"]`** on the combined store. For a | ||
| single-experiment predict run the split step is optional — the combined | ||
| `embeddings.zarr` is already per-experiment. | ||
| - Downstream analyses (dimensionality reduction, linear classifiers, MMD, | ||
| pseudotime) consume the per-experiment zarrs and are documented in | ||
| [evaluation.md](evaluation.md) and [pseudotime.md](pseudotime.md). | ||
|
|
||
| ## Triplet vs parquet (MultiExperimentDataModule) | ||
|
|
||
| | Aspect | Triplet path (this doc) | Parquet path (evaluation.md) | | ||
| | ------------------- | ------------------------------------------------ | -------------------------------------------------- | | ||
| | Data entry point | `data_path` zarr + `tracks_path` | `cell_index.parquet` (built + preprocessed) | | ||
| | Setup cost | reads tracking + zarr shape at init | reads parquet only at init | | ||
| | Focus / z window | caller sets `z_range`; `z_reduction` collapses | per-FOV `z_extraction_window` from `focus_slice` | | ||
| | 2D-from-3D | `z_reduction: mip` / `center` | `BatchedChannelWiseZReductiond` in normalizations | | ||
| | Pixel rescaling | `reference_pixel_size` | `reference_pixel_size_xy_um` | | ||
| | Best for | ad-hoc predict over a single zarr + tracking | large multi-experiment runs, reproducible recipes | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.