From 4a06de01af333bef7039aef05b62a73918c4c11d Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Sat, 21 Feb 2026 00:21:49 -0500 Subject: [PATCH 01/18] Add ESMFold backend: inference + attention/activation trace export + ICE SLURM runner --- README.md | 10 + docs/esmfold.md | 111 ++++++ docs/hpc_ice.md | 105 ++++++ environment-mac.yml | 33 ++ environment.yml | 6 +- openfold/model/structure_module.py | 7 +- openfold/resources/__init__.py | 2 + openfold/resources/stereo_chemical_props.txt | 345 +++++++++++++++++++ openfold/utils/kernel/attention_core.py | 39 ++- requirements-esmfold.txt | 6 + run_pretrained_esmf.py | 125 +++++++ scripts/hpc/ice/run_esmf_ice.slurm | 52 +++ tests/test_esmf_smoke.py | 188 ++++++++++ vizfold/__init__.py | 1 + vizfold/backends/__init__.py | 9 + vizfold/backends/base.py | 52 +++ vizfold/backends/esmfold/__init__.py | 12 + vizfold/backends/esmfold/hooks.py | 134 +++++++ vizfold/backends/esmfold/inference.py | 283 +++++++++++++++ vizfold/backends/esmfold/schema.py | 108 ++++++ vizfold/backends/esmfold/trace_adapter.py | 181 ++++++++++ 21 files changed, 1791 insertions(+), 18 deletions(-) create mode 100644 docs/esmfold.md create mode 100644 docs/hpc_ice.md create mode 100644 environment-mac.yml create mode 100644 openfold/resources/__init__.py create mode 100644 openfold/resources/stereo_chemical_props.txt create mode 100644 requirements-esmfold.txt create mode 100644 run_pretrained_esmf.py create mode 100644 scripts/hpc/ice/run_esmf_ice.slurm create mode 100644 tests/test_esmf_smoke.py create mode 100644 vizfold/__init__.py create mode 100644 vizfold/backends/__init__.py create mode 100644 vizfold/backends/base.py create mode 100644 vizfold/backends/esmfold/__init__.py create mode 100644 vizfold/backends/esmfold/hooks.py create mode 100644 vizfold/backends/esmfold/inference.py create mode 100644 vizfold/backends/esmfold/schema.py create mode 100644 vizfold/backends/esmfold/trace_adapter.py diff --git a/README.md b/README.md index ec15a886..c8c517d0 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ This repository has two main components: --- +## How do I test running OpenFold / VizFold? + +| Where | How | +|-------|-----| +| **Locally** | Use **`viz_attention_demo_base.ipynb`**. Open it, set `BASE_DATA_DIR` (or the template MMCIF path in the inference command), run the setup once (e.g. `bash scripts/download_alphafold_params.sh openfold/resources`), then run the cells. Cell 2 runs OpenFold + attention extraction; cells 3–4 run the VizFold visualizations. | +| **HPC with CyberShuttle** | Use **`viz_attention_demo.ipynb`**. It uses Airavata magics to request a GPU runtime (`cybershuttle.yml`), then clones the [attention-viz-demo](https://github.com/vizfold/attention-viz-demo) repo and runs the same pipeline there. Set `BASE_DATA_DIR` to your cluster’s AlphaFold DB path (e.g. `/depot/itap/datasets/alphafold/db`). | +| **HPC without CyberShuttle** | Same workflow as local, but from the cluster: clone this repo, create the env (e.g. from `cybershuttle.yml` or OpenFold docs), download params, then run `run_pretrained_openfold.py` via your job scheduler (e.g. `sbatch`). Optionally run the `visualize_attention_*` scripts on the outputs, or run `viz_attention_demo_base.ipynb` in Jupyter on the cluster if available. | + +--- + Link to Openfold implimentation - [README_vizfold_openfold.md](https://github.com/vizfold/vizfold-foundation/blob/main/README_vizfold_openfold.md) --- diff --git a/docs/esmfold.md b/docs/esmfold.md new file mode 100644 index 00000000..34a925d5 --- /dev/null +++ b/docs/esmfold.md @@ -0,0 +1,111 @@ +# ESMFold backend + +The ESMFold backend runs [ESMFold](https://github.com/facebookresearch/esm) (via `fair-esm`) and writes **VizFold-compatible** trace archives: structure + optional attention and activation tensors with metadata. + +## Install + +fair-esm does not install all ESMFold dependencies by default. Use either: + +**Option A – conda (recommended)** +Use `environment-mac.yml` (Mac) or `environment.yml` (Linux): + +```bash +conda env create -f environment-mac.yml # or environment.yml on Linux +conda activate openfold-env +``` + +**Option B – pip (after PyTorch is installed)** +From repo root: + +```bash +pip install -r requirements-esmfold.txt +# Optional: pip install -e . for vizfold package +``` + +`requirements-esmfold.txt` includes: dm-tree, einops, omegaconf, fair-esm. + +## Run locally + +**Structure only (fast):** + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/esmf_6KWC \ + --trace_mode none +``` + +**Structure + attention + activations:** + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/esmf_6KWC \ + --model esmfold_v1 \ + --device cuda \ + --trace_mode attention+activations \ + --layers all \ + --save_fp16 +``` + +**Limit layers/heads (saves memory and disk):** + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/esmf_6KWC \ + --trace_mode attention \ + --layers 0,1,2,5 \ + --heads 0,1,2 +``` + +## Output layout + +After a run, `--out` contains: + +``` +outputs/esmf_6KWC/ + meta.json # Run metadata (backend, model, shapes, seed, etc.) + structure/ + predicted.pdb # Predicted structure (PDB) + predicted.pt # Optional coordinate tensor + trace/ + attention/ + layer_000.pt + layer_001.pt + ... + activations/ + layer_000.pt + ... + index.json # Maps layer/head to path, dtype, shape + logs.txt # Log lines from the run +``` + +## meta.json + +Includes: + +- `backend`, `model_name`, `date_time`, `device`, `dtype` +- `sequence_length`, `input_fasta_hash`, `input_fasta_path` +- `layer_count`, `head_count`, `trace_mode`, `tensor_format` (fp16/fp32) +- `shapes_recorded`: per-file shapes for attention and activations +- `seed`, `deterministic` (if set) +- `repo_commit` (if run from a git repo) + +## Reproducibility + +- `--seed 42` fixes the PyTorch RNG. +- `--deterministic` sets CuDNN deterministic mode (can be slower). + +Both are recorded in `meta.json`. + +## Long sequences + +Attention storage is O(N²). For long proteins the script warns and suggests: + +- `--trace_mode activations` (no attention), or +- `--layers 0,1,2` to save only a few layers. + +## Running on ICE (SLURM) + +See [hpc_ice.md](hpc_ice.md) for batch submission, environment setup, and a short smoke test. diff --git a/docs/hpc_ice.md b/docs/hpc_ice.md new file mode 100644 index 00000000..6f3801a8 --- /dev/null +++ b/docs/hpc_ice.md @@ -0,0 +1,105 @@ +# Running VizFold (ESMFold) on ICE + +This guide covers running the ESMFold backend and trace export on an ICE cluster via SLURM. + +## Prerequisites + +- Access to ICE with GPU nodes +- Conda (or module environment) with PyTorch (CUDA), and `fair-esm` installed + +## Environment + +### Option A: Conda (simpler) + +```bash +conda create -n vizfold python=3.10 +conda activate vizfold +conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia +pip install fair-esm +# From repo root: +pip install -e . +``` + +### Option B: Container (if ICE supports Apptainer/Singularity) + +Use a image that includes PyTorch + fair-esm. Not required; mention in job script if available. + +## Interactive GPU session (debugging) + +Request an interactive GPU node: + +```bash +salloc --gres=gpu:1 --cpus-per-task=8 --mem=48G --time=02:00:00 +``` + +Then: + +```bash +module load cuda # or your site’s module +conda activate vizfold +cd /path/to/vizfold-foundation +python -c "import torch; print(torch.cuda.is_available())" +``` + +## Submitting a batch job + +1. Set environment variables (or edit `scripts/hpc/ice/run_esmf_ice.slurm`): + + - `FASTA` – path to input FASTA (single sequence) + - `OUTDIR` – where to write outputs (e.g. `outputs/esmf_6KWC`) + - `TRACE_MODE` – `none`, `attention`, `activations`, or `attention+activations` + +2. Submit: + +```bash +export FASTA=examples/monomer/fasta_dir_6KWC/6KWC.fasta +export OUTDIR=outputs/esmf_6KWC +sbatch scripts/hpc/ice/run_esmf_ice.slurm +``` + +3. Monitor: + +```bash +squeue -u $USER +``` + +4. Logs and outputs: + +- SLURM stdout/stderr: `outputs/logs/esmf_.out` and `.err` +- Run outputs: under `OUTDIR`: `meta.json`, `structure/`, `trace/`, `logs.txt` + +## Minimal smoke test (<5 minutes) + +To verify the job runs without using much GPU time: + +```bash +# Create a tiny FASTA (e.g. 50 residues) +echo -e ">tiny\nMKFLKFSLLTAVLLSVVFAFSSCGDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" > /tmp/tiny.fasta +export FASTA=/tmp/tiny.fasta +export OUTDIR=outputs/esmf_smoke +export TRACE_MODE=none +sbatch scripts/hpc/ice/run_esmf_ice.slurm +``` + +Then run with trace: + +```bash +export TRACE_MODE=attention +# Optional: limit layers to speed up +# (add --layers 0,1 to the script or run CLI manually) +``` + +## Common issues + +| Issue | What to check | +|-------|----------------| +| CUDA not found | Load correct `cuda` module; `nvidia-smi` on the node | +| `torch` not seeing GPU | Install PyTorch with CUDA: `conda install pytorch pytorch-cuda=...` | +| Missing packages | `pip install fair-esm`; run from repo root or `pip install -e .` | +| Disk quota | Use `OUTDIR` on scratch or project space, not home if limited | +| Job killed (OOM) | Increase `--mem` or use shorter sequence / `--trace_mode none` | + +## Partition and resources + +- Use your site’s GPU partition name in the script if different (e.g. `#SBATCH --partition=gpu`). +- Adjust `--time`, `--mem`, and `--cpus-per-task` to match queue limits and job size. diff --git a/environment-mac.yml b/environment-mac.yml new file mode 100644 index 00000000..77247692 --- /dev/null +++ b/environment-mac.yml @@ -0,0 +1,33 @@ +# macOS (incl. Apple Silicon) — use this on osx-arm64 instead of environment.yml +# environment.yml is for Linux + NVIDIA GPU (e.g. ICE cluster). +name: openfold-env +channels: + - conda-forge + - bioconda + - pytorch +dependencies: + - python=3.10 + - pip + - openmm + - pdbfixer + - pytorch-lightning + - biopython + - numpy + - pandas + - PyYAML + - requests + - scipy + - tqdm + - typing-extensions + - wandb + - modelcif==0.7 + - awscli + - ml-collections + - aria2 + - git + - pytorch::pytorch # CPU/MPS on Mac (no CUDA) + - pip: + - dm-tree==0.1.6 + - einops # required by fair-esm/ESMFold + - omegaconf # required by fair-esm/ESMFold + - fair-esm # ESMFold backend diff --git a/environment.yml b/environment.yml index e02d1b4d..18f73883 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,5 @@ -name: openfold-env +# Linux + NVIDIA GPU (e.g. ICE). On macOS (Apple Silicon) use environment-mac.yml instead. +name: openfold-env channels: - conda-forge - bioconda @@ -38,3 +39,6 @@ dependencies: - dm-tree==0.1.6 - git+https://github.com/NVIDIA/dllogger.git - flash-attn==2.6.3 + - einops # required by fair-esm/ESMFold + - omegaconf # required by fair-esm/ESMFold + - fair-esm # ESMFold backend (run_pretrained_esmf.py) diff --git a/openfold/model/structure_module.py b/openfold/model/structure_module.py index 2bd4c162..7ad6079d 100644 --- a/openfold/model/structure_module.py +++ b/openfold/model/structure_module.py @@ -44,7 +44,10 @@ flatten_final_dims, ) -attn_core_inplace_cuda = importlib.import_module("attn_core_inplace_cuda") +try: + attn_core_inplace_cuda = importlib.import_module("attn_core_inplace_cuda") +except ModuleNotFoundError: + attn_core_inplace_cuda = None class AngleResnetBlock(nn.Module): @@ -432,7 +435,7 @@ def forward( # [*, H, N_res, N_res] pt_att = permute_final_dims(pt_att, (2, 0, 1)) - if (inplace_safe): + if (inplace_safe and attn_core_inplace_cuda is not None): a += pt_att del pt_att a += square_mask.unsqueeze(-3) diff --git a/openfold/resources/__init__.py b/openfold/resources/__init__.py new file mode 100644 index 00000000..5ea9ce9f --- /dev/null +++ b/openfold/resources/__init__.py @@ -0,0 +1,2 @@ +# openfold.resources: package data (e.g. stereo_chemical_props.txt) +# Used by openfold.np.residue_constants and by fair-esm/ESMFold when it imports openfold. diff --git a/openfold/resources/stereo_chemical_props.txt b/openfold/resources/stereo_chemical_props.txt new file mode 100644 index 00000000..25262efd --- /dev/null +++ b/openfold/resources/stereo_chemical_props.txt @@ -0,0 +1,345 @@ +Bond Residue Mean StdDev +CA-CB ALA 1.520 0.021 +N-CA ALA 1.459 0.020 +CA-C ALA 1.525 0.026 +C-O ALA 1.229 0.019 +CA-CB ARG 1.535 0.022 +CB-CG ARG 1.521 0.027 +CG-CD ARG 1.515 0.025 +CD-NE ARG 1.460 0.017 +NE-CZ ARG 1.326 0.013 +CZ-NH1 ARG 1.326 0.013 +CZ-NH2 ARG 1.326 0.013 +N-CA ARG 1.459 0.020 +CA-C ARG 1.525 0.026 +C-O ARG 1.229 0.019 +CA-CB ASN 1.527 0.026 +CB-CG ASN 1.506 0.023 +CG-OD1 ASN 1.235 0.022 +CG-ND2 ASN 1.324 0.025 +N-CA ASN 1.459 0.020 +CA-C ASN 1.525 0.026 +C-O ASN 1.229 0.019 +CA-CB ASP 1.535 0.022 +CB-CG ASP 1.513 0.021 +CG-OD1 ASP 1.249 0.023 +CG-OD2 ASP 1.249 0.023 +N-CA ASP 1.459 0.020 +CA-C ASP 1.525 0.026 +C-O ASP 1.229 0.019 +CA-CB CYS 1.526 0.013 +CB-SG CYS 1.812 0.016 +N-CA CYS 1.459 0.020 +CA-C CYS 1.525 0.026 +C-O CYS 1.229 0.019 +CA-CB GLU 1.535 0.022 +CB-CG GLU 1.517 0.019 +CG-CD GLU 1.515 0.015 +CD-OE1 GLU 1.252 0.011 +CD-OE2 GLU 1.252 0.011 +N-CA GLU 1.459 0.020 +CA-C GLU 1.525 0.026 +C-O GLU 1.229 0.019 +CA-CB GLN 1.535 0.022 +CB-CG GLN 1.521 0.027 +CG-CD GLN 1.506 0.023 +CD-OE1 GLN 1.235 0.022 +CD-NE2 GLN 1.324 0.025 +N-CA GLN 1.459 0.020 +CA-C GLN 1.525 0.026 +C-O GLN 1.229 0.019 +N-CA GLY 1.456 0.015 +CA-C GLY 1.514 0.016 +C-O GLY 1.232 0.016 +CA-CB HIS 1.535 0.022 +CB-CG HIS 1.492 0.016 +CG-ND1 HIS 1.369 0.015 +CG-CD2 HIS 1.353 0.017 +ND1-CE1 HIS 1.343 0.025 +CD2-NE2 HIS 1.415 0.021 +CE1-NE2 HIS 1.322 0.023 +N-CA HIS 1.459 0.020 +CA-C HIS 1.525 0.026 +C-O HIS 1.229 0.019 +CA-CB ILE 1.544 0.023 +CB-CG1 ILE 1.536 0.028 +CB-CG2 ILE 1.524 0.031 +CG1-CD1 ILE 1.500 0.069 +N-CA ILE 1.459 0.020 +CA-C ILE 1.525 0.026 +C-O ILE 1.229 0.019 +CA-CB LEU 1.533 0.023 +CB-CG LEU 1.521 0.029 +CG-CD1 LEU 1.514 0.037 +CG-CD2 LEU 1.514 0.037 +N-CA LEU 1.459 0.020 +CA-C LEU 1.525 0.026 +C-O LEU 1.229 0.019 +CA-CB LYS 1.535 0.022 +CB-CG LYS 1.521 0.027 +CG-CD LYS 1.520 0.034 +CD-CE LYS 1.508 0.025 +CE-NZ LYS 1.486 0.025 +N-CA LYS 1.459 0.020 +CA-C LYS 1.525 0.026 +C-O LYS 1.229 0.019 +CA-CB MET 1.535 0.022 +CB-CG MET 1.509 0.032 +CG-SD MET 1.807 0.026 +SD-CE MET 1.774 0.056 +N-CA MET 1.459 0.020 +CA-C MET 1.525 0.026 +C-O MET 1.229 0.019 +CA-CB PHE 1.535 0.022 +CB-CG PHE 1.509 0.017 +CG-CD1 PHE 1.383 0.015 +CG-CD2 PHE 1.383 0.015 +CD1-CE1 PHE 1.388 0.020 +CD2-CE2 PHE 1.388 0.020 +CE1-CZ PHE 1.369 0.019 +CE2-CZ PHE 1.369 0.019 +N-CA PHE 1.459 0.020 +CA-C PHE 1.525 0.026 +C-O PHE 1.229 0.019 +CA-CB PRO 1.531 0.020 +CB-CG PRO 1.495 0.050 +CG-CD PRO 1.502 0.033 +CD-N PRO 1.474 0.014 +N-CA PRO 1.468 0.017 +CA-C PRO 1.524 0.020 +C-O PRO 1.228 0.020 +CA-CB SER 1.525 0.015 +CB-OG SER 1.418 0.013 +N-CA SER 1.459 0.020 +CA-C SER 1.525 0.026 +C-O SER 1.229 0.019 +CA-CB THR 1.529 0.026 +CB-OG1 THR 1.428 0.020 +CB-CG2 THR 1.519 0.033 +N-CA THR 1.459 0.020 +CA-C THR 1.525 0.026 +C-O THR 1.229 0.019 +CA-CB TRP 1.535 0.022 +CB-CG TRP 1.498 0.018 +CG-CD1 TRP 1.363 0.014 +CG-CD2 TRP 1.432 0.017 +CD1-NE1 TRP 1.375 0.017 +NE1-CE2 TRP 1.371 0.013 +CD2-CE2 TRP 1.409 0.012 +CD2-CE3 TRP 1.399 0.015 +CE2-CZ2 TRP 1.393 0.017 +CE3-CZ3 TRP 1.380 0.017 +CZ2-CH2 TRP 1.369 0.019 +CZ3-CH2 TRP 1.396 0.016 +N-CA TRP 1.459 0.020 +CA-C TRP 1.525 0.026 +C-O TRP 1.229 0.019 +CA-CB TYR 1.535 0.022 +CB-CG TYR 1.512 0.015 +CG-CD1 TYR 1.387 0.013 +CG-CD2 TYR 1.387 0.013 +CD1-CE1 TYR 1.389 0.015 +CD2-CE2 TYR 1.389 0.015 +CE1-CZ TYR 1.381 0.013 +CE2-CZ TYR 1.381 0.013 +CZ-OH TYR 1.374 0.017 +N-CA TYR 1.459 0.020 +CA-C TYR 1.525 0.026 +C-O TYR 1.229 0.019 +CA-CB VAL 1.543 0.021 +CB-CG1 VAL 1.524 0.021 +CB-CG2 VAL 1.524 0.021 +N-CA VAL 1.459 0.020 +CA-C VAL 1.525 0.026 +C-O VAL 1.229 0.019 +- + +Angle Residue Mean StdDev +N-CA-CB ALA 110.1 1.4 +CB-CA-C ALA 110.1 1.5 +N-CA-C ALA 111.0 2.7 +CA-C-O ALA 120.1 2.1 +N-CA-CB ARG 110.6 1.8 +CB-CA-C ARG 110.4 2.0 +CA-CB-CG ARG 113.4 2.2 +CB-CG-CD ARG 111.6 2.6 +CG-CD-NE ARG 111.8 2.1 +CD-NE-CZ ARG 123.6 1.4 +NE-CZ-NH1 ARG 120.3 0.5 +NE-CZ-NH2 ARG 120.3 0.5 +NH1-CZ-NH2 ARG 119.4 1.1 +N-CA-C ARG 111.0 2.7 +CA-C-O ARG 120.1 2.1 +N-CA-CB ASN 110.6 1.8 +CB-CA-C ASN 110.4 2.0 +CA-CB-CG ASN 113.4 2.2 +CB-CG-ND2 ASN 116.7 2.4 +CB-CG-OD1 ASN 121.6 2.0 +ND2-CG-OD1 ASN 121.9 2.3 +N-CA-C ASN 111.0 2.7 +CA-C-O ASN 120.1 2.1 +N-CA-CB ASP 110.6 1.8 +CB-CA-C ASP 110.4 2.0 +CA-CB-CG ASP 113.4 2.2 +CB-CG-OD1 ASP 118.3 0.9 +CB-CG-OD2 ASP 118.3 0.9 +OD1-CG-OD2 ASP 123.3 1.9 +N-CA-C ASP 111.0 2.7 +CA-C-O ASP 120.1 2.1 +N-CA-CB CYS 110.8 1.5 +CB-CA-C CYS 111.5 1.2 +CA-CB-SG CYS 114.2 1.1 +N-CA-C CYS 111.0 2.7 +CA-C-O CYS 120.1 2.1 +N-CA-CB GLU 110.6 1.8 +CB-CA-C GLU 110.4 2.0 +CA-CB-CG GLU 113.4 2.2 +CB-CG-CD GLU 114.2 2.7 +CG-CD-OE1 GLU 118.3 2.0 +CG-CD-OE2 GLU 118.3 2.0 +OE1-CD-OE2 GLU 123.3 1.2 +N-CA-C GLU 111.0 2.7 +CA-C-O GLU 120.1 2.1 +N-CA-CB GLN 110.6 1.8 +CB-CA-C GLN 110.4 2.0 +CA-CB-CG GLN 113.4 2.2 +CB-CG-CD GLN 111.6 2.6 +CG-CD-OE1 GLN 121.6 2.0 +CG-CD-NE2 GLN 116.7 2.4 +OE1-CD-NE2 GLN 121.9 2.3 +N-CA-C GLN 111.0 2.7 +CA-C-O GLN 120.1 2.1 +N-CA-C GLY 113.1 2.5 +CA-C-O GLY 120.6 1.8 +N-CA-CB HIS 110.6 1.8 +CB-CA-C HIS 110.4 2.0 +CA-CB-CG HIS 113.6 1.7 +CB-CG-ND1 HIS 123.2 2.5 +CB-CG-CD2 HIS 130.8 3.1 +CG-ND1-CE1 HIS 108.2 1.4 +ND1-CE1-NE2 HIS 109.9 2.2 +CE1-NE2-CD2 HIS 106.6 2.5 +NE2-CD2-CG HIS 109.2 1.9 +CD2-CG-ND1 HIS 106.0 1.4 +N-CA-C HIS 111.0 2.7 +CA-C-O HIS 120.1 2.1 +N-CA-CB ILE 110.8 2.3 +CB-CA-C ILE 111.6 2.0 +CA-CB-CG1 ILE 111.0 1.9 +CB-CG1-CD1 ILE 113.9 2.8 +CA-CB-CG2 ILE 110.9 2.0 +CG1-CB-CG2 ILE 111.4 2.2 +N-CA-C ILE 111.0 2.7 +CA-C-O ILE 120.1 2.1 +N-CA-CB LEU 110.4 2.0 +CB-CA-C LEU 110.2 1.9 +CA-CB-CG LEU 115.3 2.3 +CB-CG-CD1 LEU 111.0 1.7 +CB-CG-CD2 LEU 111.0 1.7 +CD1-CG-CD2 LEU 110.5 3.0 +N-CA-C LEU 111.0 2.7 +CA-C-O LEU 120.1 2.1 +N-CA-CB LYS 110.6 1.8 +CB-CA-C LYS 110.4 2.0 +CA-CB-CG LYS 113.4 2.2 +CB-CG-CD LYS 111.6 2.6 +CG-CD-CE LYS 111.9 3.0 +CD-CE-NZ LYS 111.7 2.3 +N-CA-C LYS 111.0 2.7 +CA-C-O LYS 120.1 2.1 +N-CA-CB MET 110.6 1.8 +CB-CA-C MET 110.4 2.0 +CA-CB-CG MET 113.3 1.7 +CB-CG-SD MET 112.4 3.0 +CG-SD-CE MET 100.2 1.6 +N-CA-C MET 111.0 2.7 +CA-C-O MET 120.1 2.1 +N-CA-CB PHE 110.6 1.8 +CB-CA-C PHE 110.4 2.0 +CA-CB-CG PHE 113.9 2.4 +CB-CG-CD1 PHE 120.8 0.7 +CB-CG-CD2 PHE 120.8 0.7 +CD1-CG-CD2 PHE 118.3 1.3 +CG-CD1-CE1 PHE 120.8 1.1 +CG-CD2-CE2 PHE 120.8 1.1 +CD1-CE1-CZ PHE 120.1 1.2 +CD2-CE2-CZ PHE 120.1 1.2 +CE1-CZ-CE2 PHE 120.0 1.8 +N-CA-C PHE 111.0 2.7 +CA-C-O PHE 120.1 2.1 +N-CA-CB PRO 103.3 1.2 +CB-CA-C PRO 111.7 2.1 +CA-CB-CG PRO 104.8 1.9 +CB-CG-CD PRO 106.5 3.9 +CG-CD-N PRO 103.2 1.5 +CA-N-CD PRO 111.7 1.4 +N-CA-C PRO 112.1 2.6 +CA-C-O PRO 120.2 2.4 +N-CA-CB SER 110.5 1.5 +CB-CA-C SER 110.1 1.9 +CA-CB-OG SER 111.2 2.7 +N-CA-C SER 111.0 2.7 +CA-C-O SER 120.1 2.1 +N-CA-CB THR 110.3 1.9 +CB-CA-C THR 111.6 2.7 +CA-CB-OG1 THR 109.0 2.1 +CA-CB-CG2 THR 112.4 1.4 +OG1-CB-CG2 THR 110.0 2.3 +N-CA-C THR 111.0 2.7 +CA-C-O THR 120.1 2.1 +N-CA-CB TRP 110.6 1.8 +CB-CA-C TRP 110.4 2.0 +CA-CB-CG TRP 113.7 1.9 +CB-CG-CD1 TRP 127.0 1.3 +CB-CG-CD2 TRP 126.6 1.3 +CD1-CG-CD2 TRP 106.3 0.8 +CG-CD1-NE1 TRP 110.1 1.0 +CD1-NE1-CE2 TRP 109.0 0.9 +NE1-CE2-CD2 TRP 107.3 1.0 +CE2-CD2-CG TRP 107.3 0.8 +CG-CD2-CE3 TRP 133.9 0.9 +NE1-CE2-CZ2 TRP 130.4 1.1 +CE3-CD2-CE2 TRP 118.7 1.2 +CD2-CE2-CZ2 TRP 122.3 1.2 +CE2-CZ2-CH2 TRP 117.4 1.0 +CZ2-CH2-CZ3 TRP 121.6 1.2 +CH2-CZ3-CE3 TRP 121.2 1.1 +CZ3-CE3-CD2 TRP 118.8 1.3 +N-CA-C TRP 111.0 2.7 +CA-C-O TRP 120.1 2.1 +N-CA-CB TYR 110.6 1.8 +CB-CA-C TYR 110.4 2.0 +CA-CB-CG TYR 113.4 1.9 +CB-CG-CD1 TYR 121.0 0.6 +CB-CG-CD2 TYR 121.0 0.6 +CD1-CG-CD2 TYR 117.9 1.1 +CG-CD1-CE1 TYR 121.3 0.8 +CG-CD2-CE2 TYR 121.3 0.8 +CD1-CE1-CZ TYR 119.8 0.9 +CD2-CE2-CZ TYR 119.8 0.9 +CE1-CZ-CE2 TYR 119.8 1.6 +CE1-CZ-OH TYR 120.1 2.7 +CE2-CZ-OH TYR 120.1 2.7 +N-CA-C TYR 111.0 2.7 +CA-C-O TYR 120.1 2.1 +N-CA-CB VAL 111.5 2.2 +CB-CA-C VAL 111.4 1.9 +CA-CB-CG1 VAL 110.9 1.5 +CA-CB-CG2 VAL 110.9 1.5 +CG1-CB-CG2 VAL 110.9 1.6 +N-CA-C VAL 111.0 2.7 +CA-C-O VAL 120.1 2.1 +- + +Non-bonded distance Minimum Dist Tolerance +C-C 3.4 1.5 +C-N 3.25 1.5 +C-S 3.5 1.5 +C-O 3.22 1.5 +N-N 3.1 1.5 +N-S 3.35 1.5 +N-O 3.07 1.5 +O-S 3.32 1.5 +O-O 3.04 1.5 +S-S 2.03 1.0 +- diff --git a/openfold/utils/kernel/attention_core.py b/openfold/utils/kernel/attention_core.py index 362ea303..c72dff2d 100644 --- a/openfold/utils/kernel/attention_core.py +++ b/openfold/utils/kernel/attention_core.py @@ -17,7 +17,11 @@ import torch -attn_core_inplace_cuda = importlib.import_module("attn_core_inplace_cuda") +try: + attn_core_inplace_cuda = importlib.import_module("attn_core_inplace_cuda") +except ModuleNotFoundError: + # CUDA extension not built (e.g. macOS or CPU-only); use PyTorch softmax + attn_core_inplace_cuda = None SUPPORTED_DTYPES = [torch.float32, torch.bfloat16] @@ -44,11 +48,14 @@ def forward(ctx, q, k, v, bias_1=None, bias_2=None): if(bias_2 is not None): attention_logits += bias_2 - attn_core_inplace_cuda.forward_( - attention_logits, - reduce(mul, attention_logits.shape[:-1]), - attention_logits.shape[-1], - ) + if attn_core_inplace_cuda is not None: + attn_core_inplace_cuda.forward_( + attention_logits, + reduce(mul, attention_logits.shape[:-1]), + attention_logits.shape[-1], + ) + else: + attention_logits = torch.nn.functional.softmax(attention_logits, dim=-1) o = torch.matmul(attention_logits, v) @@ -64,18 +71,20 @@ def backward(ctx, grad_output): grad_q = grad_k = grad_v = grad_bias_1 = grad_bias_2 = None grad_v = torch.matmul( - attention_logits.transpose(-1, -2), + attention_logits.transpose(-1, -2), grad_output ) - attn_core_inplace_cuda.backward_( - attention_logits, - grad_output.contiguous(), - v.contiguous(), # v is implicitly transposed in the kernel - reduce(mul, attention_logits.shape[:-1]), - attention_logits.shape[-1], - grad_output.shape[-1], - ) + if attn_core_inplace_cuda is not None: + attn_core_inplace_cuda.backward_( + attention_logits, + grad_output.contiguous(), + v.contiguous(), # v is implicitly transposed in the kernel + reduce(mul, attention_logits.shape[:-1]), + attention_logits.shape[-1], + grad_output.shape[-1], + ) + # else: attention_logits is already softmax; backward through matmul uses it as-is if(ctx.bias_1_shape is not None): grad_bias_1 = torch.sum( diff --git a/requirements-esmfold.txt b/requirements-esmfold.txt new file mode 100644 index 00000000..0d5678a7 --- /dev/null +++ b/requirements-esmfold.txt @@ -0,0 +1,6 @@ +# ESMFold backend (run_pretrained_esmf.py). Install after PyTorch. +# Use: pip install -r requirements-esmfold.txt +dm-tree>=0.1.6 +einops +omegaconf +fair-esm diff --git a/run_pretrained_esmf.py b/run_pretrained_esmf.py new file mode 100644 index 00000000..5a52cbeb --- /dev/null +++ b/run_pretrained_esmf.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +ESMFold inference with VizFold-compatible trace export. + +Produces an archive under --out with: + meta.json, structure/, trace/attention/, trace/activations/, trace/index.json, logs.txt + +Example: + python run_pretrained_esmf.py \\ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \\ + --out outputs/esmf_6KWC \\ + --model esmfold_v1 \\ + --device cuda \\ + --trace_mode attention+activations \\ + --layers all \\ + --save_fp16 +""" +import argparse +import os +import sys + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run ESMFold and export VizFold-compatible traces.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--fasta", + type=str, + required=True, + help="Path to FASTA file (single sequence).", + ) + parser.add_argument( + "--out", + type=str, + required=True, + help="Output directory (will be created).", + ) + parser.add_argument( + "--model", + type=str, + default="esmfold_v1", + help="Model name (e.g. esmfold_v1).", + ) + parser.add_argument( + "--device", + type=str, + default=None, + help="Device: cuda, cuda:0, cpu. Default: cuda if available else cpu.", + ) + parser.add_argument( + "--trace_mode", + type=str, + default="attention+activations", + choices=["none", "attention", "activations", "attention+activations"], + help="What to extract: none (structure only), attention, activations, or both.", + ) + parser.add_argument( + "--layers", + type=str, + default="all", + help="Layers to save: 'all' or '0,1,2' or '0:12'.", + ) + parser.add_argument( + "--heads", + type=str, + default="all", + help="Heads to save: 'all' or '0,1,2'.", + ) + parser.add_argument( + "--save_fp16", + action="store_true", + help="Save trace tensors in fp16 to reduce size.", + ) + parser.add_argument( + "--seed", + type=int, + default=None, + help="Random seed for reproducibility.", + ) + parser.add_argument( + "--deterministic", + action="store_true", + help="Use deterministic CuDNN (may reduce speed).", + ) + args = parser.parse_args() + + if not os.path.isfile(args.fasta): + print(f"Error: FASTA not found: {args.fasta}", file=sys.stderr) + return 1 + + if args.device is None: + try: + import torch + args.device = "cuda" if torch.cuda.is_available() else "cpu" + except ImportError: + args.device = "cpu" + + try: + from vizfold.backends.esmfold.inference import ESMFoldRunner + except ImportError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + runner = ESMFoldRunner( + model_name=args.model, + device=args.device, + seed=args.seed, + deterministic=args.deterministic, + ) + runner.run( + fasta_path=args.fasta, + out_dir=args.out, + trace_mode=args.trace_mode, + layers=args.layers, + heads=args.heads, + save_fp16=args.save_fp16, + ) + print(f"Done. Outputs in {args.out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/hpc/ice/run_esmf_ice.slurm b/scripts/hpc/ice/run_esmf_ice.slurm new file mode 100644 index 00000000..f50e2e85 --- /dev/null +++ b/scripts/hpc/ice/run_esmf_ice.slurm @@ -0,0 +1,52 @@ +#!/bin/bash +#SBATCH --job-name=esmf_vizfold +#SBATCH --gres=gpu:1 +#SBATCH --cpus-per-task=8 +#SBATCH --mem=48G +#SBATCH --time=06:00:00 +#SBATCH --output=outputs/logs/esmf_%j.out +#SBATCH --error=outputs/logs/esmf_%j.err +# +# ESMFold inference with VizFold trace export on ICE. +# Usage: +# export FASTA=/path/to/seq.fasta OUTDIR=outputs/esmf_run +# sbatch run_esmf_ice.slurm +# Or override on the command line: +# sbatch run_esmf_ice.slurm (uses FASTA and OUTDIR from environment) +# +# For a quick smoke test (<5 min): use a short FASTA and --trace_mode none + +set -e + +# Defaults if not set +FASTA="${FASTA:-examples/monomer/fasta_dir_6KWC/6KWC.fasta}" +OUTDIR="${OUTDIR:-outputs/esmf_ice_${SLURM_JOB_ID}}" +TRACE_MODE="${TRACE_MODE:-attention+activations}" +DEVICE="${DEVICE:-cuda}" + +mkdir -p outputs/logs +mkdir -p "$OUTDIR" + +# Uncomment and set for your ICE environment: +# module load cuda/11.8 +# module load anaconda3 +# source activate vizfold + +# Or conda in project: +# eval "$(conda shell.bash hook)" +# conda activate vizfold + +cd "${VIZFOLD_REPO:-.}" +echo "FASTA=$FASTA OUTDIR=$OUTDIR TRACE_MODE=$TRACE_MODE" +echo "Running ESMFold..." + +python run_pretrained_esmf.py \ + --fasta "$FASTA" \ + --out "$OUTDIR" \ + --model esmfold_v1 \ + --device "$DEVICE" \ + --trace_mode "$TRACE_MODE" \ + --layers all \ + --save_fp16 + +echo "Done. Outputs in $OUTDIR" diff --git a/tests/test_esmf_smoke.py b/tests/test_esmf_smoke.py new file mode 100644 index 00000000..7cf3d7cc --- /dev/null +++ b/tests/test_esmf_smoke.py @@ -0,0 +1,188 @@ +""" +Smoke test for ESMFold backend: CLI and output layout. + +Runs in CPU mode on a tiny sequence so CI stays fast. +Skips actual ESMFold inference if fair-esm is not installed. + +Run with: pytest tests/test_esmf_smoke.py -v +Or without pytest: python tests/test_esmf_smoke.py +""" +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +# Repo root +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +try: + import pytest +except ImportError: + pytest = None + + +def _has_esm() -> bool: + try: + import esm # noqa: F401 + return True + except ImportError: + return False + + +def test_esmf_import(): + """Backend and runner can be imported when fair-esm is present.""" + if not _has_esm(): + return # skip when no fair-esm + from vizfold.backends.esmfold.inference import ESMFoldRunner + from vizfold.backends.esmfold.schema import build_meta, write_meta + assert ESMFoldRunner is not None + meta = build_meta( + backend="esmfold", + model_name="esmfold_v1", + out_dir="/tmp", + fasta_path=None, + device="cpu", + dtype="float32", + sequence_length=10, + fasta_hash="abc", + layer_count=1, + head_count=4, + trace_mode="none", + trace_formats=[], + shapes_recorded={}, + ) + assert meta["backend"] == "esmfold" + assert "date_time" in meta + + +def test_cli_help(): + """CLI runs and shows help.""" + result = subprocess.run( + [sys.executable, str(REPO_ROOT / "run_pretrained_esmf.py"), "--help"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert "--fasta" in result.stdout + assert "--trace_mode" in result.stdout + + +def test_cli_missing_fasta(): + """CLI exits non-zero when FASTA is missing.""" + result = subprocess.run( + [ + sys.executable, + str(REPO_ROOT / "run_pretrained_esmf.py"), + "--fasta", "/nonexistent.fasta", + "--out", "/tmp/out_esmf_smoke", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "not found" in result.stderr or "Error" in result.stderr + + +def test_esmf_smoke_run_cpu(tmp_path=None): + """ + Run ESMFold on a tiny sequence (CPU), check output layout. + Kept minimal so it stays under a few minutes. + """ + if not _has_esm(): + return # skip when no fair-esm + if tmp_path is None: + tmp_path = Path(tempfile.mkdtemp()) + fasta = tmp_path / "tiny.fasta" + fasta.write_text(">tiny\nMKFLKFSLLTAVLLSVVFAFSSCGDDDD\n") + out_dir = tmp_path / "out" + out_dir.mkdir(parents=True, exist_ok=True) + + result = subprocess.run( + [ + sys.executable, + str(REPO_ROOT / "run_pretrained_esmf.py"), + "--fasta", str(fasta), + "--out", str(out_dir), + "--device", "cpu", + "--trace_mode", "none", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=300, + ) + assert result.returncode == 0, (result.stdout, result.stderr) + + assert (out_dir / "meta.json").exists() + assert (out_dir / "logs.txt").exists() + # Structure may or may not exist depending on model output + structure_dir = out_dir / "structure" + if structure_dir.exists(): + assert list(structure_dir.iterdir()) + + +# Pytest decorators when available +if pytest is not None: + test_esmf_import = pytest.mark.skipif(not _has_esm(), reason="fair-esm not installed")(test_esmf_import) + test_esmf_smoke_run_cpu = pytest.mark.skipif(not _has_esm(), reason="fair-esm not installed")(test_esmf_smoke_run_cpu) + + +if __name__ == "__main__": + # Run without pytest + failed = [] + # CLI help + print("test_cli_help ...", end=" ") + try: + test_cli_help() + print("ok") + except AssertionError as e: + print("FAIL", e) + failed.append("test_cli_help") + # CLI missing fasta + print("test_cli_missing_fasta ...", end=" ") + try: + test_cli_missing_fasta() + print("ok") + except AssertionError as e: + print("FAIL", e) + failed.append("test_cli_missing_fasta") + # Schema/build_meta (no ESM needed) + print("test_schema_build_meta ...", end=" ") + try: + from vizfold.backends.esmfold.schema import build_meta + meta = build_meta( + backend="esmfold", model_name="x", out_dir="/tmp", fasta_path=None, + device="cpu", dtype="float32", sequence_length=10, fasta_hash="h", + layer_count=1, head_count=4, trace_mode="none", trace_formats=[], + shapes_recorded={}, + ) + assert meta["backend"] == "esmfold" and "date_time" in meta + print("ok") + except Exception as e: + print("FAIL", e) + failed.append("test_schema_build_meta") + # ESMFold import (when fair-esm present) + print("test_esmf_import ...", end=" ") + try: + test_esmf_import() + print("ok (or skipped)") + except Exception as e: + print("FAIL", e) + failed.append("test_esmf_import") + # Full smoke run (when fair-esm present) + print("test_esmf_smoke_run_cpu ...", end=" ") + try: + test_esmf_smoke_run_cpu() + print("ok (or skipped)") + except Exception as e: + print("FAIL", e) + failed.append("test_esmf_smoke_run_cpu") + if failed: + print("Failed:", failed) + sys.exit(1) + print("All checks passed.") diff --git a/vizfold/__init__.py b/vizfold/__init__.py new file mode 100644 index 00000000..01d2067e --- /dev/null +++ b/vizfold/__init__.py @@ -0,0 +1 @@ +# VizFold: multi-backend interpretability for protein structure prediction diff --git a/vizfold/backends/__init__.py b/vizfold/backends/__init__.py new file mode 100644 index 00000000..7bc2591e --- /dev/null +++ b/vizfold/backends/__init__.py @@ -0,0 +1,9 @@ +""" +VizFold backends: pluggable inference and trace extraction. + +Each backend (openfold, esmfold) implements the base interface +so visualization and analysis can consume traces uniformly. +""" +from vizfold.backends.base import BackendBase + +__all__ = ["BackendBase"] diff --git a/vizfold/backends/base.py b/vizfold/backends/base.py new file mode 100644 index 00000000..0e1d779e --- /dev/null +++ b/vizfold/backends/base.py @@ -0,0 +1,52 @@ +""" +Backend interface for VizFold. + +Implementations (OpenFold, ESMFold) provide model loading, +inference, and trace extraction so the visualization layer +can consume outputs uniformly. +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional + +# Trace config: what to extract and where to write +TraceConfig = Dict[str, Any] + + +class BackendBase(ABC): + """Base interface for structure-prediction backends.""" + + @abstractmethod + def load_model( + self, + model_name: str, + device: str = "cpu", + dtype: Optional[str] = None, + **kwargs: Any, + ) -> Any: + """Load the model. Returns the model object (backend-specific).""" + pass + + @abstractmethod + def run_inference( + self, + fasta_path: str, + out_dir: str, + trace_cfg: Optional[TraceConfig] = None, + **kwargs: Any, + ) -> Dict[str, Any]: + """ + Run inference and optionally write traces. + + Returns a dict with at least: structure_path, meta (dict), optional trace paths. + """ + pass + + @abstractmethod + def supports_attention(self) -> bool: + """Whether this backend can extract attention maps.""" + pass + + @abstractmethod + def supports_activations(self) -> bool: + """Whether this backend can extract layer activations (hidden states).""" + pass diff --git a/vizfold/backends/esmfold/__init__.py b/vizfold/backends/esmfold/__init__.py new file mode 100644 index 00000000..fc97282e --- /dev/null +++ b/vizfold/backends/esmfold/__init__.py @@ -0,0 +1,12 @@ +""" +ESMFold backend: inference and trace export for ESMFold (fair-esm). +""" +# Lazy import so schema/ can be used without requiring torch/fair-esm +def __getattr__(name): + if name == "ESMFoldRunner": + from vizfold.backends.esmfold.inference import ESMFoldRunner + return ESMFoldRunner + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = ["ESMFoldRunner"] diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py new file mode 100644 index 00000000..a10b5177 --- /dev/null +++ b/vizfold/backends/esmfold/hooks.py @@ -0,0 +1,134 @@ +""" +Hook-based extraction of attention and hidden states from ESMFold. + +Strategy: + 1. Prefer model outputs (output_attentions=True, output_hidden_states=True) if available. + 2. Fall back to forward hooks on known modules with a clear warning. +""" +import warnings +from typing import Any, Callable, Dict, List, Optional, Tuple + +import torch +import torch.nn as nn + + +class ESMFoldTraceCollector: + """ + Collects attention weights and/or hidden states during ESMFold forward. + + Can use either returned outputs (preferred) or registered hooks. + """ + + def __init__( + self, + want_attention: bool = True, + want_activations: bool = True, + layer_indices: Optional[List[int]] = None, + head_indices: Optional[List[int]] = None, + ): + self.want_attention = want_attention + self.want_activations = want_activations + self.layer_indices = layer_indices # None => all + self.head_indices = head_indices + self.attention: Dict[str, torch.Tensor] = {} + self.activations: Dict[str, torch.Tensor] = {} + self._handles: List[Any] = [] + + def clear(self) -> None: + self.attention.clear() + self.activations.clear() + + def _store_attention(self, name: str, attn: torch.Tensor, layer_idx: int) -> None: + if self.layer_indices is not None and layer_idx not in self.layer_indices: + return + key = f"layer_{layer_idx:03d}" + if key not in self.attention: + self.attention[key] = attn.detach() + else: + self.attention[key] = torch.stack([self.attention[key], attn.detach()], dim=0) + + def _store_activation(self, name: str, h: torch.Tensor, layer_idx: int) -> None: + if self.layer_indices is not None and layer_idx not in self.layer_indices: + return + key = f"layer_{layer_idx:03d}" + self.activations[key] = h.detach() + + def try_use_outputs( + self, + outputs: Any, + model_name: str = "esmfold", + ) -> Tuple[bool, bool]: + """ + Try to populate from model forward return value. + Returns (got_attention, got_activations). + """ + got_attn, got_act = False, False + if hasattr(outputs, "attentions") and outputs.attentions is not None and self.want_attention: + for i, attn in enumerate(outputs.attentions): + if attn is not None: + self._store_attention("output", attn, i) + got_attn = True + if hasattr(outputs, "hidden_states") and outputs.hidden_states is not None and self.want_activations: + for i, h in enumerate(outputs.hidden_states): + if h is not None: + self._store_activation("output", h, i) + got_act = True + if self.want_attention and not got_attn and hasattr(outputs, "states"): + # ESMFold may return trunk states; no standard attentions + pass + return got_attn, got_act + + def register_hooks(self, model: nn.Module) -> None: + """ + Register forward hooks to capture attention/activations if not from outputs. + ESMFold structure: esm trunk -> folding trunk. We hook transformer layers. + """ + layer_idx = [0] + + def _make_attn_hook(idx: int) -> Callable: + def hook(module: nn.Module, inp: Any, out: Any) -> None: + if isinstance(out, tuple): + # Many attention modules return (output, attn_weights) + for o in out: + if o is not None and o.dim() >= 3 and o.shape[0] == out[0].shape[0]: + self._store_attention("hook", o, idx) + break + elif out is not None and out.dim() >= 3: + self._store_attention("hook", out, idx) + return hook + + def _make_act_hook(idx: int) -> Callable: + def hook(module: nn.Module, inp: Any, out: Any) -> None: + if isinstance(out, tuple): + out = out[0] + if out is not None and out.dim() >= 2: + self._store_activation("hook", out, idx) + return hook + + for name, module in model.named_modules(): + if "attention" in name.lower() and hasattr(module, "forward"): + try: + h = module.register_forward_hook(_make_attn_hook(layer_idx[0])) + self._handles.append(h) + layer_idx[0] += 1 + except Exception: + pass + if "layer" in name.lower() and "encoder" in name.lower() and hasattr(module, "forward"): + try: + h = module.register_forward_hook(_make_act_hook(layer_idx[0])) + self._handles.append(h) + layer_idx[0] += 1 + except Exception: + pass + + if self._handles: + warnings.warn( + "ESMFold: using forward hooks for trace extraction; " + "output_attentions/output_hidden_states were not available.", + UserWarning, + ) + + def remove_hooks(self) -> None: + for h in self._handles: + h.remove() + self._handles.clear() diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py new file mode 100644 index 00000000..3c267c7e --- /dev/null +++ b/vizfold/backends/esmfold/inference.py @@ -0,0 +1,283 @@ +""" +ESMFold inference: load model, run forward, optionally extract traces. + +Uses fair-esm (esm.pretrained.esmfold_v1) when available. +""" +import os +import sys +import warnings +from typing import Any, Dict, List, Optional, Tuple + +import torch + +from vizfold.backends.esmfold.hooks import ESMFoldTraceCollector +from vizfold.backends.esmfold.schema import _read_fasta_and_hash +from vizfold.backends.esmfold.trace_adapter import ( + build_and_write_meta, + write_structure, + write_traces, + write_trace_summary, +) + +# Optional: fair-esm +try: + import esm + from esm.pretrained import esmfold_v1 + HAS_ESM = True +except ImportError: + HAS_ESM = False + esm = None + esmfold_v1 = None + + +LONG_SEQ_WARN_THRESHOLD = 400 # N^2 attention warning + + +def _parse_layers_arg(layers_arg: Optional[str]) -> Optional[List[int]]: + if not layers_arg or layers_arg.lower() == "all": + return None + indices = [] + for part in layers_arg.split(","): + part = part.strip() + if ":" in part: + a, b = part.split(":", 1) + indices.extend(range(int(a), int(b))) + else: + indices.append(int(part)) + return sorted(set(indices)) + + +def _parse_heads_arg(heads_arg: Optional[str]) -> Optional[List[int]]: + if not heads_arg or heads_arg.lower() == "all": + return None + return [int(x.strip()) for x in heads_arg.split(",")] + + +def read_fasta(fasta_path: str) -> Tuple[str, str]: + """Return (sequence, id).""" + seq, _ = _read_fasta_and_hash(fasta_path) + with open(fasta_path) as f: + for line in f: + if line.startswith(">"): + return seq, line[1:].strip().split()[0] + return seq, "seq" + + +class ESMFoldRunner: + """ + Runs ESMFold inference and writes VizFold-compatible output. + + Not a full BackendBase implementation; used by run_pretrained_esmf.py. + """ + + def __init__( + self, + model_name: str = "esmfold_v1", + device: str = "cpu", + dtype: Optional[str] = None, + seed: Optional[int] = None, + deterministic: bool = False, + ): + if not HAS_ESM: + raise RuntimeError( + "ESMFold backend requires fair-esm. Install with: pip install fair-esm" + ) + self.model_name = model_name + self.device = device + self.dtype = dtype or "float32" + self.seed = seed + self.deterministic = deterministic + self._model = None + self._alphabet = None + + def load_model(self) -> Any: + if self._model is not None: + return self._model + if self.seed is not None: + torch.manual_seed(self.seed) + if self.deterministic: + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + warnings.warn("Deterministic mode may reduce speed.", UserWarning) + + model, alphabet = esmfold_v1() + self._alphabet = alphabet + model = model.eval() + dtype = torch.float16 if self.dtype == "float16" else torch.float32 + model = model.to(device=self.device, dtype=dtype) + self._model = model + return model + + def run( + self, + fasta_path: str, + out_dir: str, + trace_mode: str = "attention+activations", + layers: Optional[str] = None, + heads: Optional[str] = None, + save_fp16: bool = False, + log_path: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Run inference and write structure + optional traces. + + trace_mode: "attention" | "activations" | "attention+activations" | "none" + layers: "all" or "0,1,2" or "0:12" + heads: "all" or "0,1,2" + """ + os.makedirs(out_dir, exist_ok=True) + if log_path is None: + log_path = os.path.join(out_dir, "logs.txt") + + def log(msg: str) -> None: + with open(log_path, "a") as f: + f.write(msg + "\n") + print(msg) + + seq, seq_id = read_fasta(fasta_path) + seq_len = len(seq) + _, fasta_hash = _read_fasta_and_hash(fasta_path) + + if seq_len > LONG_SEQ_WARN_THRESHOLD and "attention" in trace_mode: + log( + f"Warning: sequence length {seq_len} > {LONG_SEQ_WARN_THRESHOLD}. " + "Attention storage is N^2; consider --layers 0,1 or --trace_mode activations." + ) + + model = self.load_model() + want_attn = "attention" in trace_mode + want_act = "activations" in trace_mode + layer_list = _parse_layers_arg(layers) + head_list = _parse_heads_arg(heads) + + collector = ESMFoldTraceCollector( + want_attention=want_attn, + want_activations=want_act, + layer_indices=layer_list, + head_indices=head_list, + ) + + # Try model forward with optional outputs + with torch.no_grad(): + # ESMFold in fair-esm: tokenize with model's alphabet + alphabet = self._alphabet + if alphabet is None: + alphabet = esm.Alphabet.from_architecture("ESM-1b") + batch_converter = alphabet.get_batch_converter() + _, _, batch_tokens = batch_converter([(seq_id, seq)]) + batch_tokens = batch_tokens.to(device=self.device) + + kwargs = {} + if hasattr(model, "forward"): + sig = getattr(model.forward, "__wrapped__", model.forward) + try: + import inspect + params = inspect.signature(model.forward).parameters + if "output_attentions" in params: + kwargs["output_attentions"] = want_attn + if "output_hidden_states" in params: + kwargs["output_hidden_states"] = want_act + except Exception: + pass + + out = model(batch_tokens, **kwargs) + + got_attn, got_act = collector.try_use_outputs(out, self.model_name) + if (want_attn and not got_attn) or (want_act and not got_act): + collector.register_hooks(model) + collector.clear() + out = model(batch_tokens) + collector.try_use_outputs(out, self.model_name) + collector.remove_hooks() + + # Structure output + pdb_str = None + coords = None + if hasattr(out, "positions"): + pos = out.positions + if pos is not None: + coords = pos + if hasattr(out, "to_pdb"): + try: + pdb_str = out.to_pdb(out)[0] if hasattr(out.to_pdb(out), "__getitem__") else out.to_pdb(out) + except Exception: + pass + if pdb_str is None and hasattr(out, "pdb_string"): + pdb_str = getattr(out, "pdb_string", None) + if pdb_str is None and coords is not None: + pdb_str = _coords_to_minimal_pdb(coords, seq) + if pdb_str is None: + log("Warning: no PDB output from model; structure/ may be incomplete.") + + struct_paths = write_structure(out_dir, pdb_str, coords) + log(f"Structure written: {struct_paths}") + + # Traces + shapes_recorded = {"attention": {}, "activations": {}} + if trace_mode != "none" and (collector.attention or collector.activations): + attn_idx, act_idx = write_traces( + out_dir, + collector, + save_fp16=save_fp16, + layer_indices=layer_list, + head_indices=head_list, + ) + for k, v in attn_idx.items(): + shapes_recorded["attention"][k] = v.get("shape", []) + for k, v in act_idx.items(): + shapes_recorded["activations"][k] = v.get("shape", []) + try: + write_trace_summary(out_dir, collector) + except Exception: + pass + + layer_count = max( + len(collector.attention), + len(collector.activations), + 1, + ) + head_count = 0 + if collector.attention: + first_attn = next(iter(collector.attention.values())) + if first_attn.dim() >= 3: + head_count = first_attn.shape[-3] if first_attn.dim() == 3 else first_attn.shape[1] + + build_and_write_meta( + out_dir=out_dir, + model_name=self.model_name, + fasta_path=os.path.abspath(fasta_path), + device=self.device, + dtype=self.dtype, + seq_len=seq_len, + fasta_hash=fasta_hash, + layer_count=layer_count, + head_count=head_count, + trace_mode=trace_mode, + shapes_recorded=shapes_recorded, + seed=self.seed, + deterministic=self.deterministic, + save_fp16=save_fp16, + ) + log("meta.json written.") + + return { + "structure": struct_paths, + "out_dir": out_dir, + "trace_mode": trace_mode, + } + + +def _coords_to_minimal_pdb(coords: torch.Tensor, seq: str) -> str: + """Write minimal CA-only PDB from coords [N, 3] or [N, 37, 3].""" + if coords.dim() == 3: + ca = coords[:, 1, :] # assume atom37 order: N, CA, C, ... + else: + ca = coords + lines = [] + for i in range(ca.shape[0]): + a = ca[i].cpu().numpy() + res = seq[i] if i < len(seq) else "X" + lines.append( + f"ATOM {i+1:5d} CA {res} A{i+1:4d} {a[0]:8.3f}{a[1]:8.3f}{a[2]:8.3f} 1.00 0.00 C" + ) + return "\n".join(lines) + "\n" diff --git a/vizfold/backends/esmfold/schema.py b/vizfold/backends/esmfold/schema.py new file mode 100644 index 00000000..5a934ec7 --- /dev/null +++ b/vizfold/backends/esmfold/schema.py @@ -0,0 +1,108 @@ +""" +Trace schema and metadata helpers for ESMFold outputs. + +Ensures VizFold-compatible archive format and publication-grade metadata. +""" +import hashlib +import json +import os +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +def _git_head(repo_path: Optional[str] = None) -> Optional[str]: + try: + cmd = ["git", "rev-parse", "HEAD"] + if repo_path: + cmd = ["git", "-C", repo_path, "rev-parse", "HEAD"] + return subprocess.check_output(cmd, text=True).strip() + except Exception: + return None + + +def _read_fasta_and_hash(path: str) -> Tuple[str, str]: + with open(path) as f: + raw = f.read() + lines = [l.strip() for l in raw.splitlines() if l.strip() and not l.startswith(">")] + seq = "".join(lines) + h = hashlib.sha256(raw.encode()).hexdigest()[:16] + return seq, h + + +def build_meta( + *, + backend: str = "esmfold", + model_name: str, + out_dir: str, + fasta_path: Optional[str] = None, + device: str, + dtype: str, + sequence_length: int, + fasta_hash: str, + layer_count: int, + head_count: int, + trace_mode: str, + trace_formats: List[str], + shapes_recorded: Dict[str, Any], + seed: Optional[int] = None, + deterministic: bool = False, + save_fp16: bool = False, + repo_path: Optional[str] = None, +) -> Dict[str, Any]: + """ + Build meta.json content for a VizFold-compatible run. + + shapes_recorded: e.g. {"attention": {"layer_000": [num_heads, N, N]}, "activations": {...}} + """ + meta: Dict[str, Any] = { + "backend": backend, + "model_name": model_name, + "date_time": datetime.now(timezone.utc).isoformat(), + "device": device, + "dtype": dtype, + "sequence_length": sequence_length, + "input_fasta_hash": fasta_hash, + "layer_count": layer_count, + "head_count": head_count, + "trace_mode": trace_mode, + "tensor_format": "fp16" if save_fp16 else "fp32", + "shapes_recorded": shapes_recorded, + } + if fasta_path: + meta["input_fasta_path"] = fasta_path + if seed is not None: + meta["seed"] = seed + if deterministic: + meta["deterministic"] = True + commit = _git_head(repo_path) + if commit: + meta["repo_commit"] = commit + return meta + + +def write_meta(meta: Dict[str, Any], out_dir: str) -> str: + path = os.path.join(out_dir, "meta.json") + with open(path, "w") as f: + json.dump(meta, f, indent=2) + return path + + +def write_trace_index( + out_dir: str, + attention: Dict[str, Any], + activations: Dict[str, Any], +) -> str: + """ + Write trace/index.json mapping layers/heads to file paths and shapes. + """ + index = { + "attention": attention, + "activations": activations, + } + path = os.path.join(out_dir, "trace", "index.json") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + json.dump(index, f, indent=2) + return path diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py new file mode 100644 index 00000000..d08b7fc7 --- /dev/null +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -0,0 +1,181 @@ +""" +Writes ESMFold traces into VizFold-compatible archive layout. + +Layout: + out_dir/ + meta.json + structure/ + predicted.pdb + predicted.pt (optional) + trace/ + attention/ + layer_000.pt + ... + activations/ + layer_000.pt + ... + index.json + logs.txt (caller appends) +""" +import os +from typing import Any, Dict, Optional, Tuple + +import torch + +from vizfold.backends.esmfold.schema import ( + build_meta, + write_meta, + write_trace_index, +) +from vizfold.backends.esmfold.hooks import ESMFoldTraceCollector + + +def _save_tensor(path: str, t: torch.Tensor, save_fp16: bool = False) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + if save_fp16 and t.dtype == torch.float32: + t = t.half() + torch.save(t.cpu(), path) + + +def write_structure(out_dir: str, pdb_str: Optional[str], coords: Optional[torch.Tensor]) -> Dict[str, str]: + base = os.path.join(out_dir, "structure") + os.makedirs(base, exist_ok=True) + paths = {} + if pdb_str: + pdb_path = os.path.join(base, "predicted.pdb") + with open(pdb_path, "w") as f: + f.write(pdb_str) + paths["pdb"] = pdb_path + if coords is not None: + pt_path = os.path.join(base, "predicted.pt") + torch.save(coords.cpu(), pt_path) + paths["coords"] = pt_path + return paths + + +def write_traces( + out_dir: str, + collector: ESMFoldTraceCollector, + save_fp16: bool = False, + layer_indices: Optional[list] = None, + head_indices: Optional[list] = None, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + Write attention and activations from collector to trace/attention/ and trace/activations/. + Returns (attention_index, activations_index) for index.json. + """ + trace_root = os.path.join(out_dir, "trace") + attn_dir = os.path.join(trace_root, "attention") + act_dir = os.path.join(trace_root, "activations") + os.makedirs(attn_dir, exist_ok=True) + os.makedirs(act_dir, exist_ok=True) + + attention_index: Dict[str, Any] = {} + activations_index: Dict[str, Any] = {} + + for key, t in collector.attention.items(): + path = os.path.join(attn_dir, f"{key}.pt") + if head_indices is not None and t.dim() >= 3: + t = t[:, head_indices, ...] + _save_tensor(path, t, save_fp16=save_fp16) + attention_index[key] = { + "path": path, + "dtype": str(t.dtype), + "shape": list(t.shape), + } + + for key, t in collector.activations.items(): + path = os.path.join(act_dir, f"{key}.pt") + _save_tensor(path, t, save_fp16=save_fp16) + activations_index[key] = { + "path": path, + "dtype": str(t.dtype), + "shape": list(t.shape), + } + + write_trace_index(out_dir, attention_index, activations_index) + return attention_index, activations_index + + +def write_trace_summary( + out_dir: str, + collector: "ESMFoldTraceCollector", +) -> Optional[str]: + """ + Write trace/summary.json with per-layer attention entropy, mean/std, sparsity proxy, + and activation norms. Cheap to compute; useful for analysis. + """ + import json + import numpy as np + summary = {"attention": {}, "activations": {}} + for key, t in collector.attention.items(): + a = t.float().cpu().numpy() + # a: [..., heads, N, N] + if a.size == 0: + continue + if a.ndim == 3: + a = a[np.newaxis, ...] + # Per-layer (first dim after batch) stats + for i in range(a.shape[0]): + layer_key = f"{key}_slice{i}" if a.shape[0] > 1 else key + block = a[i] + if block.ndim >= 3: + # heads, N, N + ent = -np.sum(block * np.log(block + 1e-12), axis=(-2, -1)).mean() + summary["attention"][layer_key] = { + "mean": float(block.mean()), + "std": float(block.std()), + "entropy_proxy": float(ent), + "sparsity_proxy": float((block < 1e-5).mean()), + } + for key, t in collector.activations.items(): + h = t.float().cpu().numpy() + if h.size == 0: + continue + summary["activations"][key] = { + "norm_mean": float(np.sqrt((h ** 2).mean())), + "mean": float(h.mean()), + "std": float(h.std()), + } + path = os.path.join(out_dir, "trace", "summary.json") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + json.dump(summary, f, indent=2) + return path + + +def build_and_write_meta( + out_dir: str, + model_name: str, + fasta_path: str, + device: str, + dtype: str, + seq_len: int, + fasta_hash: str, + layer_count: int, + head_count: int, + trace_mode: str, + shapes_recorded: Dict[str, Any], + seed: Optional[int] = None, + deterministic: bool = False, + save_fp16: bool = False, +) -> str: + meta = build_meta( + backend="esmfold", + model_name=model_name, + out_dir=out_dir, + fasta_path=fasta_path, + device=device, + dtype=dtype, + sequence_length=seq_len, + fasta_hash=fasta_hash, + layer_count=layer_count, + head_count=head_count, + trace_mode=trace_mode, + trace_formats=["pt"], # VizFold trace format + shapes_recorded=shapes_recorded, + seed=seed, + deterministic=deterministic, + save_fp16=save_fp16, + ) + return write_meta(meta, out_dir) From 947bece0eb8368682ddd9d28068bbd8637d03fcb Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Sat, 21 Feb 2026 01:26:13 -0500 Subject: [PATCH 02/18] Switch ESMFold backend to HuggingFace implementation to avoid OpenFold build dependency --- docs/.DS_Store | Bin 0 -> 6148 bytes docs/esmfold.md | 8 +- docs/hpc_ice.md | 6 +- environment.yml | 4 +- requirements-esmfold.txt | 8 +- run_pretrained_esmf.py | 6 +- scripts/hpc/ice/run_esmf_ice.slurm | 2 +- tests/test_esmf_smoke.py | 20 +-- vizfold/backends/esmfold/inference.py | 190 +++++++++++++++++--------- 9 files changed, 151 insertions(+), 93 deletions(-) create mode 100644 docs/.DS_Store diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f1054d31e643e516df4b6bf2dfdde14e3d393721 GIT binary patch literal 6148 zcmeHK%SyvQ6rE|KO({Ya3SADkE!Y=|xCycTfDv7&)P$58Of#iP?V=R2)*tdq{2uR} znTW-@6|wii%(>5*%z?~l-IVu|j-5Wy{lZ?o5jBt@n!$gK)znR!y z2mE%6#caaHEc^QX;WUY}yxo51m8!L~TeE9+!@l<)<-*T{e4cs1>=vynDU-0&gYY^Y zEr!nixlHpQPDe9U5Jw|Oxx0zeNG?1%PoqrL`Z{3O?fTGZE|;C&NlSG5omERLPrL1w z==Y9StGaz~cyx9-d`c#%e9?q*;9JR_!4h6U`CQMlKT8vtj=@*vmwAN505L!e5ChxG zfH@QF#`aP`D<=ksfgc#a{XsxObPX07)z$$WUY{{;A)<=_`4&ox+R)a8t;nPD6=bNP7TYIg7omCm@Uk$Pf)7+7bZs!a#a{|oqKHa_y# zOUNPyh=G5`0JjGIz=K7Zv-R8Z@T?Wk9-yIMUV#b-=xdh%FmNAfs-TVw)FIC`SZKsi T(67n?=^~&Ap^g~%1qMC=0.1.6 -einops -omegaconf -fair-esm +# Uses HuggingFace Transformers to avoid OpenFold build dependency. +# Use: pip install torch && pip install -r requirements-esmfold.txt +transformers>=4.36.0 diff --git a/run_pretrained_esmf.py b/run_pretrained_esmf.py index 5a52cbeb..fb23846c 100644 --- a/run_pretrained_esmf.py +++ b/run_pretrained_esmf.py @@ -9,7 +9,7 @@ python run_pretrained_esmf.py \\ --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \\ --out outputs/esmf_6KWC \\ - --model esmfold_v1 \\ + --model facebook/esmfold_v1 \\ --device cuda \\ --trace_mode attention+activations \\ --layers all \\ @@ -40,8 +40,8 @@ def main() -> int: parser.add_argument( "--model", type=str, - default="esmfold_v1", - help="Model name (e.g. esmfold_v1).", + default="facebook/esmfold_v1", + help="HuggingFace model id (e.g. facebook/esmfold_v1).", ) parser.add_argument( "--device", diff --git a/scripts/hpc/ice/run_esmf_ice.slurm b/scripts/hpc/ice/run_esmf_ice.slurm index f50e2e85..acbe4652 100644 --- a/scripts/hpc/ice/run_esmf_ice.slurm +++ b/scripts/hpc/ice/run_esmf_ice.slurm @@ -43,7 +43,7 @@ echo "Running ESMFold..." python run_pretrained_esmf.py \ --fasta "$FASTA" \ --out "$OUTDIR" \ - --model esmfold_v1 \ + --model facebook/esmfold_v1 \ --device "$DEVICE" \ --trace_mode "$TRACE_MODE" \ --layers all \ diff --git a/tests/test_esmf_smoke.py b/tests/test_esmf_smoke.py index 7cf3d7cc..6d2eabfc 100644 --- a/tests/test_esmf_smoke.py +++ b/tests/test_esmf_smoke.py @@ -2,7 +2,7 @@ Smoke test for ESMFold backend: CLI and output layout. Runs in CPU mode on a tiny sequence so CI stays fast. -Skips actual ESMFold inference if fair-esm is not installed. +Skips actual ESMFold inference if transformers is not installed. Run with: pytest tests/test_esmf_smoke.py -v Or without pytest: python tests/test_esmf_smoke.py @@ -26,22 +26,22 @@ def _has_esm() -> bool: try: - import esm # noqa: F401 + from transformers import EsmForProteinFolding # noqa: F401 return True except ImportError: return False def test_esmf_import(): - """Backend and runner can be imported when fair-esm is present.""" + """Backend and runner can be imported when transformers is present.""" if not _has_esm(): - return # skip when no fair-esm + return # skip when no transformers from vizfold.backends.esmfold.inference import ESMFoldRunner from vizfold.backends.esmfold.schema import build_meta, write_meta assert ESMFoldRunner is not None meta = build_meta( backend="esmfold", - model_name="esmfold_v1", + model_name="facebook/esmfold_v1", out_dir="/tmp", fasta_path=None, device="cpu", @@ -94,7 +94,7 @@ def test_esmf_smoke_run_cpu(tmp_path=None): Kept minimal so it stays under a few minutes. """ if not _has_esm(): - return # skip when no fair-esm + return # skip when no transformers if tmp_path is None: tmp_path = Path(tempfile.mkdtemp()) fasta = tmp_path / "tiny.fasta" @@ -128,8 +128,8 @@ def test_esmf_smoke_run_cpu(tmp_path=None): # Pytest decorators when available if pytest is not None: - test_esmf_import = pytest.mark.skipif(not _has_esm(), reason="fair-esm not installed")(test_esmf_import) - test_esmf_smoke_run_cpu = pytest.mark.skipif(not _has_esm(), reason="fair-esm not installed")(test_esmf_smoke_run_cpu) + test_esmf_import = pytest.mark.skipif(not _has_esm(), reason="transformers not installed")(test_esmf_import) + test_esmf_smoke_run_cpu = pytest.mark.skipif(not _has_esm(), reason="transformers not installed")(test_esmf_smoke_run_cpu) if __name__ == "__main__": @@ -166,7 +166,7 @@ def test_esmf_smoke_run_cpu(tmp_path=None): except Exception as e: print("FAIL", e) failed.append("test_schema_build_meta") - # ESMFold import (when fair-esm present) + # ESMFold import (when transformers present) print("test_esmf_import ...", end=" ") try: test_esmf_import() @@ -174,7 +174,7 @@ def test_esmf_smoke_run_cpu(tmp_path=None): except Exception as e: print("FAIL", e) failed.append("test_esmf_import") - # Full smoke run (when fair-esm present) + # Full smoke run (when transformers present) print("test_esmf_smoke_run_cpu ...", end=" ") try: test_esmf_smoke_run_cpu() diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 3c267c7e..1f41f9a9 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -1,7 +1,7 @@ """ ESMFold inference: load model, run forward, optionally extract traces. -Uses fair-esm (esm.pretrained.esmfold_v1) when available. +Uses HuggingFace Transformers (EsmForProteinFolding) to avoid OpenFold build dependency. """ import os import sys @@ -19,18 +19,27 @@ write_trace_summary, ) -# Optional: fair-esm +# HuggingFace ESMFold try: - import esm - from esm.pretrained import esmfold_v1 + from transformers import AutoTokenizer, EsmForProteinFolding HAS_ESM = True except ImportError: HAS_ESM = False - esm = None - esmfold_v1 = None + AutoTokenizer = None # type: ignore + EsmForProteinFolding = None # type: ignore + +# PDB conversion (bundled in transformers) +def _get_pdb_utils(): + try: + from transformers.models.esm.openfold_utils.feats import atom14_to_atom37 + from transformers.models.esm.openfold_utils.protein import Protein as OFProtein, to_pdb + return atom14_to_atom37, OFProtein, to_pdb + except ImportError: + return None, None, None LONG_SEQ_WARN_THRESHOLD = 400 # N^2 attention warning +HF_MODEL_DEFAULT = "facebook/esmfold_v1" def _parse_layers_arg(layers_arg: Optional[str]) -> Optional[List[int]]: @@ -67,12 +76,12 @@ class ESMFoldRunner: """ Runs ESMFold inference and writes VizFold-compatible output. - Not a full BackendBase implementation; used by run_pretrained_esmf.py. + Uses HuggingFace EsmForProteinFolding (no fair-esm / OpenFold build). """ def __init__( self, - model_name: str = "esmfold_v1", + model_name: str = HF_MODEL_DEFAULT, device: str = "cpu", dtype: Optional[str] = None, seed: Optional[int] = None, @@ -80,7 +89,7 @@ def __init__( ): if not HAS_ESM: raise RuntimeError( - "ESMFold backend requires fair-esm. Install with: pip install fair-esm" + "ESMFold backend requires transformers. Install with: pip install transformers torch" ) self.model_name = model_name self.device = device @@ -88,7 +97,7 @@ def __init__( self.seed = seed self.deterministic = deterministic self._model = None - self._alphabet = None + self._tokenizer = None def load_model(self) -> Any: if self._model is not None: @@ -96,17 +105,19 @@ def load_model(self) -> Any: if self.seed is not None: torch.manual_seed(self.seed) if self.deterministic: - torch.backends.cudnn.deterministic = True - torch.backends.cudnn.benchmark = False + try: + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + except Exception: + pass warnings.warn("Deterministic mode may reduce speed.", UserWarning) - model, alphabet = esmfold_v1() - self._alphabet = alphabet - model = model.eval() - dtype = torch.float16 if self.dtype == "float16" else torch.float32 - model = model.to(device=self.device, dtype=dtype) - self._model = model - return model + self._tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self._model = EsmForProteinFolding.from_pretrained(self.model_name) + self._model = self._model.eval() + dtype_t = torch.float16 if self.dtype == "float16" else torch.float32 + self._model = self._model.to(device=self.device, dtype=dtype_t) + return self._model def run( self, @@ -145,6 +156,7 @@ def log(msg: str) -> None: ) model = self.load_model() + tokenizer = self._tokenizer want_attn = "attention" in trace_mode want_act = "activations" in trace_mode layer_list = _parse_layers_arg(layers) @@ -157,55 +169,107 @@ def log(msg: str) -> None: head_indices=head_list, ) - # Try model forward with optional outputs + # Tokenize: single sequence, no special tokens (per HF ESMFold usage) + inputs = tokenizer( + [seq], + return_tensors="pt", + add_special_tokens=False, + padding=False, + ) + input_ids = inputs["input_ids"].to(device=self.device) + attention_mask = inputs.get("attention_mask") + if attention_mask is not None: + attention_mask = attention_mask.to(device=self.device) + with torch.no_grad(): - # ESMFold in fair-esm: tokenize with model's alphabet - alphabet = self._alphabet - if alphabet is None: - alphabet = esm.Alphabet.from_architecture("ESM-1b") - batch_converter = alphabet.get_batch_converter() - _, _, batch_tokens = batch_converter([(seq_id, seq)]) - batch_tokens = batch_tokens.to(device=self.device) - - kwargs = {} - if hasattr(model, "forward"): - sig = getattr(model.forward, "__wrapped__", model.forward) - try: - import inspect - params = inspect.signature(model.forward).parameters - if "output_attentions" in params: - kwargs["output_attentions"] = want_attn - if "output_hidden_states" in params: - kwargs["output_hidden_states"] = want_act - except Exception: - pass - - out = model(batch_tokens, **kwargs) - - got_attn, got_act = collector.try_use_outputs(out, self.model_name) - if (want_attn and not got_attn) or (want_act and not got_act): - collector.register_hooks(model) - collector.clear() - out = model(batch_tokens) - collector.try_use_outputs(out, self.model_name) - collector.remove_hooks() - - # Structure output + kwargs = { + "input_ids": input_ids, + "attention_mask": attention_mask, + "output_attentions": want_attn, + "output_hidden_states": want_act, + } + out = model(**kwargs) + + got_attn, got_act = collector.try_use_outputs(out, "esmfold") + if (want_attn and not got_attn) or (want_act and not got_act): + # Capture from ESM trunk (model.esm) via hooks + esm_trunk = getattr(model, "esm", model) + collector.register_hooks(esm_trunk) + collector.clear() + with torch.no_grad(): + out = model( + input_ids=input_ids, + attention_mask=attention_mask, + ) + collector.try_use_outputs(out, "esmfold") + collector.remove_hooks() + + # Structure: PDB + optional coords tensor (HF returns dict-like or object) pdb_str = None coords = None - if hasattr(out, "positions"): - pos = out.positions - if pos is not None: - coords = pos - if hasattr(out, "to_pdb"): + atom14_to_atom37, OFProtein, to_pdb = _get_pdb_utils() + + def _get_out(key: str): + if hasattr(out, key): + return getattr(out, key) + if isinstance(out, dict) and key in out: + return out[key] + return None + + positions = _get_out("positions") + if positions is not None: try: - pdb_str = out.to_pdb(out)[0] if hasattr(out.to_pdb(out), "__getitem__") else out.to_pdb(out) - except Exception: - pass - if pdb_str is None and hasattr(out, "pdb_string"): - pdb_str = getattr(out, "pdb_string", None) + if atom14_to_atom37 and OFProtein is not None and to_pdb is not None: + # positions: often [num_recycling, batch, N, 14, 3]; use last recycling + pos = positions + if pos.dim() == 5: + pos = pos[-1] + final_atom37 = atom14_to_atom37(pos, out) + coords = final_atom37 + out_cpu = {} + for k in ("aatype", "atom37_atom_exists", "residue_index", "plddt", "chain_index"): + v = _get_out(k) + if v is not None: + out_cpu[k] = v.cpu().numpy() if isinstance(v, torch.Tensor) else v + pos_np = final_atom37.cpu().numpy() + for i in range(pos_np.shape[0]): + pred = OFProtein( + aatype=out_cpu["aatype"][i], + atom_positions=pos_np[i], + atom_mask=out_cpu["atom37_atom_exists"][i], + residue_index=out_cpu["residue_index"][i] + 1, + b_factors=out_cpu["plddt"][i], + chain_index=out_cpu.get("chain_index", [None] * pos_np.shape[0])[i] if "chain_index" in out_cpu else None, + ) + pdb_str = to_pdb(pred) + break + else: + pos = positions + if pos.dim() == 5: + pos = pos[-1, 0] + elif pos.dim() == 4: + pos = pos[0] + coords = pos + if coords.dim() == 4: + ca_idx = 1 + coords = coords[:, :, ca_idx, :] + pdb_str = _coords_to_minimal_pdb(coords[0], seq) + except Exception as e: + log(f"Warning: PDB conversion failed ({e}); writing minimal PDB if possible.") + if coords is not None: + try: + c = coords[0] if coords.dim() > 2 else coords + if c.dim() == 3: + c = c[:, 1, :] + pdb_str = _coords_to_minimal_pdb(c, seq) + except Exception: + pass + if pdb_str is None and coords is not None: - pdb_str = _coords_to_minimal_pdb(coords, seq) + c = coords[0] if coords.dim() > 2 else coords + if c.dim() == 3: + c = c[:, 1, :] + pdb_str = _coords_to_minimal_pdb(c, seq) if pdb_str is None: log("Warning: no PDB output from model; structure/ may be incomplete.") From e2be72e252a4509fbcf4b025d9371d68d79b6d50 Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Sat, 21 Feb 2026 01:40:43 -0500 Subject: [PATCH 03/18] Fix HF ESMFold forward: do not pass output_attentions/output_hidden_states; use hooks for traces --- vizfold/backends/esmfold/inference.py | 30 ++++++++++----------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 1f41f9a9..3ba83be9 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -181,27 +181,19 @@ def log(msg: str) -> None: if attention_mask is not None: attention_mask = attention_mask.to(device=self.device) - with torch.no_grad(): - kwargs = { - "input_ids": input_ids, - "attention_mask": attention_mask, - "output_attentions": want_attn, - "output_hidden_states": want_act, - } - out = model(**kwargs) - - got_attn, got_act = collector.try_use_outputs(out, "esmfold") - if (want_attn and not got_attn) or (want_act and not got_act): - # Capture from ESM trunk (model.esm) via hooks + # HF EsmForProteinFolding does NOT accept output_attentions / output_hidden_states. + # For traces we use hooks on model.esm; for structure-only we just run forward. + if trace_mode != "none": esm_trunk = getattr(model, "esm", model) collector.register_hooks(esm_trunk) - collector.clear() - with torch.no_grad(): - out = model( - input_ids=input_ids, - attention_mask=attention_mask, - ) - collector.try_use_outputs(out, "esmfold") + + with torch.no_grad(): + out = model( + input_ids=input_ids, + attention_mask=attention_mask, + ) + + if trace_mode != "none": collector.remove_hooks() # Structure: PDB + optional coords tensor (HF returns dict-like or object) From 0d4f5a32f237f73e0595b548df9af577158c788c Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Wed, 11 Mar 2026 16:14:39 -0400 Subject: [PATCH 04/18] Stabilize ESMFold backend: fix hook-based trace extraction, clean up pipeline - Rewrite hooks.py: target HF encoder.layer[i].attention.self directly instead of broad name-matching; monkey-patch forward to force output_attentions=True so real attention weights [B,H,N,N] are captured (not hidden states); separate attention/activation hooks with correct layer indices; slice out / tokens from attention maps - Fix _coords_to_minimal_pdb: use 3-letter residue codes (valid PDB) - Remove dead code: try_use_outputs() path, shared mutable counter - Extract structure logic into _extract_structure() method - Unify FASTA reading into single read_fasta() returning (seq, id, hash) - Wire --dtype through CLI (float32/float16 model loading) - Log runner.run() result (attention/activation layer counts) - Fix trace_adapter: correct head-slicing axis for 3D vs 4D tensors; fix entropy calculation (per-row, not per-matrix) --- run_pretrained_esmf.py | 13 +- vizfold/backends/esmfold/hooks.py | 207 ++++++++++++---------- vizfold/backends/esmfold/inference.py | 207 ++++++++++++---------- vizfold/backends/esmfold/trace_adapter.py | 9 +- 4 files changed, 244 insertions(+), 192 deletions(-) diff --git a/run_pretrained_esmf.py b/run_pretrained_esmf.py index fb23846c..1be4c0b3 100644 --- a/run_pretrained_esmf.py +++ b/run_pretrained_esmf.py @@ -16,6 +16,7 @@ --save_fp16 """ import argparse +import json import os import sys @@ -49,6 +50,13 @@ def main() -> int: default=None, help="Device: cuda, cuda:0, cpu. Default: cuda if available else cpu.", ) + parser.add_argument( + "--dtype", + type=str, + default="float32", + choices=["float32", "float16"], + help="Model dtype (float16 reduces VRAM but may lower accuracy).", + ) parser.add_argument( "--trace_mode", type=str, @@ -106,10 +114,11 @@ def main() -> int: runner = ESMFoldRunner( model_name=args.model, device=args.device, + dtype=args.dtype, seed=args.seed, deterministic=args.deterministic, ) - runner.run( + result = runner.run( fasta_path=args.fasta, out_dir=args.out, trace_mode=args.trace_mode, @@ -118,6 +127,8 @@ def main() -> int: save_fp16=args.save_fp16, ) print(f"Done. Outputs in {args.out}") + print(f" attention layers: {result.get('attention_layers', 0)}, " + f"activation layers: {result.get('activation_layers', 0)}") return 0 diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index a10b5177..d8b9f15d 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -1,10 +1,18 @@ """ -Hook-based extraction of attention and hidden states from ESMFold. +Hook-based extraction of attention weights and hidden states from HuggingFace ESMFold. -Strategy: - 1. Prefer model outputs (output_attentions=True, output_hidden_states=True) if available. - 2. Fall back to forward hooks on known modules with a clear warning. +HF EsmForProteinFolding does NOT support output_attentions / output_hidden_states. +We capture traces by registering forward hooks on the ESM-2 trunk: + + Attention weights: hook on each EsmSelfAttention module, monkey-patching + the forward to force attn_weights to be returned. + Activations: hook on each EsmLayer (full transformer block output). + +The ESM-2 tokenizer adds and tokens, so attention maps are +(seq_len+2, seq_len+2). We slice out the special tokens so stored tensors +are (seq_len, seq_len), matching the FASTA sequence. """ +import re import warnings from typing import Any, Callable, Dict, List, Optional, Tuple @@ -14,9 +22,8 @@ class ESMFoldTraceCollector: """ - Collects attention weights and/or hidden states during ESMFold forward. - - Can use either returned outputs (preferred) or registered hooks. + Collects attention weights and/or hidden states from the ESM-2 trunk + inside HuggingFace EsmForProteinFolding. """ def __init__( @@ -33,102 +40,122 @@ def __init__( self.attention: Dict[str, torch.Tensor] = {} self.activations: Dict[str, torch.Tensor] = {} self._handles: List[Any] = [] + self._patched_forwards: List[Tuple[nn.Module, Callable]] = [] def clear(self) -> None: self.attention.clear() self.activations.clear() - def _store_attention(self, name: str, attn: torch.Tensor, layer_idx: int) -> None: - if self.layer_indices is not None and layer_idx not in self.layer_indices: - return - key = f"layer_{layer_idx:03d}" - if key not in self.attention: - self.attention[key] = attn.detach() - else: - self.attention[key] = torch.stack([self.attention[key], attn.detach()], dim=0) - - def _store_activation(self, name: str, h: torch.Tensor, layer_idx: int) -> None: - if self.layer_indices is not None and layer_idx not in self.layer_indices: - return - key = f"layer_{layer_idx:03d}" - self.activations[key] = h.detach() + def _should_store_layer(self, layer_idx: int) -> bool: + return self.layer_indices is None or layer_idx in self.layer_indices - def try_use_outputs( - self, - outputs: Any, - model_name: str = "esmfold", - ) -> Tuple[bool, bool]: - """ - Try to populate from model forward return value. - Returns (got_attention, got_activations). + def register_hooks(self, esm_model: nn.Module) -> None: """ - got_attn, got_act = False, False - if hasattr(outputs, "attentions") and outputs.attentions is not None and self.want_attention: - for i, attn in enumerate(outputs.attentions): - if attn is not None: - self._store_attention("output", attn, i) - got_attn = True - if hasattr(outputs, "hidden_states") and outputs.hidden_states is not None and self.want_activations: - for i, h in enumerate(outputs.hidden_states): - if h is not None: - self._store_activation("output", h, i) - got_act = True - if self.want_attention and not got_attn and hasattr(outputs, "states"): - # ESMFold may return trunk states; no standard attentions - pass - return got_attn, got_act - - def register_hooks(self, model: nn.Module) -> None: - """ - Register forward hooks to capture attention/activations if not from outputs. - ESMFold structure: esm trunk -> folding trunk. We hook transformer layers. + Register hooks on the ESM-2 trunk (model.esm passed in). + + Targets: + - encoder.layer[i].attention.self -> attention weights [B, H, N, N] + - encoder.layer[i] -> activations [B, N, D] + + For HF ESM, EsmSelfAttention.forward() only returns attn_weights when + the framework's output capturing mechanism requests it. We monkey-patch + the forward to always emit (output, attn_weights). """ - layer_idx = [0] - - def _make_attn_hook(idx: int) -> Callable: - def hook(module: nn.Module, inp: Any, out: Any) -> None: - if isinstance(out, tuple): - # Many attention modules return (output, attn_weights) - for o in out: - if o is not None and o.dim() >= 3 and o.shape[0] == out[0].shape[0]: - self._store_attention("hook", o, idx) - break - elif out is not None and out.dim() >= 3: - self._store_attention("hook", out, idx) - return hook - - def _make_act_hook(idx: int) -> Callable: - def hook(module: nn.Module, inp: Any, out: Any) -> None: - if isinstance(out, tuple): - out = out[0] - if out is not None and out.dim() >= 2: - self._store_activation("hook", out, idx) - return hook - - for name, module in model.named_modules(): - if "attention" in name.lower() and hasattr(module, "forward"): - try: - h = module.register_forward_hook(_make_attn_hook(layer_idx[0])) - self._handles.append(h) - layer_idx[0] += 1 - except Exception: - pass - if "layer" in name.lower() and "encoder" in name.lower() and hasattr(module, "forward"): - try: - h = module.register_forward_hook(_make_act_hook(layer_idx[0])) - self._handles.append(h) - layer_idx[0] += 1 - except Exception: - pass - - if self._handles: + encoder_layers = self._find_encoder_layers(esm_model) + if not encoder_layers: warnings.warn( - "ESMFold: using forward hooks for trace extraction; " - "output_attentions/output_hidden_states were not available.", + "Could not find encoder.layer ModuleList in ESM trunk. " + "Trace extraction may not work.", UserWarning, ) + return + + for layer_idx, layer_module in enumerate(encoder_layers): + if not self._should_store_layer(layer_idx): + continue + + if self.want_attention: + self_attn = self._find_self_attention(layer_module) + if self_attn is not None: + self._patch_and_hook_attention(self_attn, layer_idx) + + if self.want_activations: + h = layer_module.register_forward_hook(self._make_activation_hook(layer_idx)) + self._handles.append(h) def remove_hooks(self) -> None: for h in self._handles: h.remove() self._handles.clear() + for module, orig_forward in self._patched_forwards: + module.forward = orig_forward + self._patched_forwards.clear() + + def _find_encoder_layers(self, esm_model: nn.Module) -> Optional[nn.ModuleList]: + """Find the nn.ModuleList of transformer layers in the ESM encoder.""" + if hasattr(esm_model, "encoder"): + enc = esm_model.encoder + if hasattr(enc, "layer") and isinstance(enc.layer, nn.ModuleList): + return enc.layer + for name, module in esm_model.named_modules(): + if isinstance(module, nn.ModuleList) and re.match(r".*encoder.*layer$", name): + return module + return None + + def _find_self_attention(self, layer_module: nn.Module) -> Optional[nn.Module]: + """Find the self-attention submodule inside a transformer layer.""" + if hasattr(layer_module, "attention"): + attn = layer_module.attention + if hasattr(attn, "self"): + return attn.self + return attn + return None + + def _patch_and_hook_attention(self, self_attn: nn.Module, layer_idx: int) -> None: + """ + Monkey-patch EsmSelfAttention.forward to always return attn_weights, + then register a hook to capture them. + + HF EsmSelfAttention returns (attn_output, attn_weights) from its + internal attention function. But the outer EsmAttention layer discards + attn_weights with `attn_output, _ = self.self(...)`. We hook self_attn + directly to get the full tuple. + """ + orig_forward = self_attn.forward + + def patched_forward(*args, **kwargs): + # Force output_attentions so the attention weights are computed + # and included in the return tuple. + kwargs["output_attentions"] = True + return orig_forward(*args, **kwargs) + + self_attn.forward = patched_forward + self._patched_forwards.append((self_attn, orig_forward)) + + h = self_attn.register_forward_hook(self._make_attention_hook(layer_idx)) + self._handles.append(h) + + def _make_attention_hook(self, layer_idx: int) -> Callable: + """Hook that captures attn_weights from (attn_output, attn_weights) tuple.""" + def hook(module: nn.Module, inp: Any, out: Any) -> None: + if not isinstance(out, tuple) or len(out) < 2: + return + attn_weights = out[1] + if attn_weights is None: + return + # attn_weights shape: [B, H, N+2, N+2] (includes and ) + # Slice out special tokens -> [B, H, N, N] + if attn_weights.dim() == 4 and attn_weights.shape[-1] >= 3: + attn_weights = attn_weights[:, :, 1:-1, 1:-1] + key = f"layer_{layer_idx:03d}" + self.attention[key] = attn_weights.detach() + return hook + + def _make_activation_hook(self, layer_idx: int) -> Callable: + """Hook that captures the transformer layer output (hidden state).""" + def hook(module: nn.Module, inp: Any, out: Any) -> None: + h = out[0] if isinstance(out, tuple) else out + if h is not None and isinstance(h, torch.Tensor) and h.dim() >= 2: + key = f"layer_{layer_idx:03d}" + self.activations[key] = h.detach() + return hook diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 3ba83be9..507d3179 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -38,9 +38,17 @@ def _get_pdb_utils(): return None, None, None -LONG_SEQ_WARN_THRESHOLD = 400 # N^2 attention warning +LONG_SEQ_WARN_THRESHOLD = 400 HF_MODEL_DEFAULT = "facebook/esmfold_v1" +AA_1TO3 = { + "A": "ALA", "R": "ARG", "N": "ASN", "D": "ASP", "C": "CYS", + "E": "GLU", "Q": "GLN", "G": "GLY", "H": "HIS", "I": "ILE", + "L": "LEU", "K": "LYS", "M": "MET", "F": "PHE", "P": "PRO", + "S": "SER", "T": "THR", "W": "TRP", "Y": "TYR", "V": "VAL", + "U": "SEC", "O": "PYL", "X": "UNK", +} + def _parse_layers_arg(layers_arg: Optional[str]) -> Optional[List[int]]: if not layers_arg or layers_arg.lower() == "all": @@ -62,14 +70,16 @@ def _parse_heads_arg(heads_arg: Optional[str]) -> Optional[List[int]]: return [int(x.strip()) for x in heads_arg.split(",")] -def read_fasta(fasta_path: str) -> Tuple[str, str]: - """Return (sequence, id).""" - seq, _ = _read_fasta_and_hash(fasta_path) +def read_fasta(fasta_path: str) -> Tuple[str, str, str]: + """Return (sequence, seq_id, fasta_hash).""" + seq, fasta_hash = _read_fasta_and_hash(fasta_path) + seq_id = "seq" with open(fasta_path) as f: for line in f: if line.startswith(">"): - return seq, line[1:].strip().split()[0] - return seq, "seq" + seq_id = line[1:].strip().split()[0] + break + return seq, seq_id, fasta_hash class ESMFoldRunner: @@ -83,7 +93,7 @@ def __init__( self, model_name: str = HF_MODEL_DEFAULT, device: str = "cpu", - dtype: Optional[str] = None, + dtype: str = "float32", seed: Optional[int] = None, deterministic: bool = False, ): @@ -93,7 +103,7 @@ def __init__( ) self.model_name = model_name self.device = device - self.dtype = dtype or "float32" + self.dtype = dtype self.seed = seed self.deterministic = deterministic self._model = None @@ -145,9 +155,8 @@ def log(msg: str) -> None: f.write(msg + "\n") print(msg) - seq, seq_id = read_fasta(fasta_path) + seq, seq_id, fasta_hash = read_fasta(fasta_path) seq_len = len(seq) - _, fasta_hash = _read_fasta_and_hash(fasta_path) if seq_len > LONG_SEQ_WARN_THRESHOLD and "attention" in trace_mode: log( @@ -169,7 +178,7 @@ def log(msg: str) -> None: head_indices=head_list, ) - # Tokenize: single sequence, no special tokens (per HF ESMFold usage) + # Tokenize (HF ESMFold: add_special_tokens=False per standard usage) inputs = tokenizer( [seq], return_tensors="pt", @@ -181,90 +190,23 @@ def log(msg: str) -> None: if attention_mask is not None: attention_mask = attention_mask.to(device=self.device) - # HF EsmForProteinFolding does NOT accept output_attentions / output_hidden_states. - # For traces we use hooks on model.esm; for structure-only we just run forward. + # HF EsmForProteinFolding does NOT accept output_attentions/output_hidden_states. + # Hooks on model.esm capture traces during the single forward pass. if trace_mode != "none": esm_trunk = getattr(model, "esm", model) collector.register_hooks(esm_trunk) with torch.no_grad(): - out = model( - input_ids=input_ids, - attention_mask=attention_mask, - ) + out = model(input_ids=input_ids, attention_mask=attention_mask) if trace_mode != "none": collector.remove_hooks() - # Structure: PDB + optional coords tensor (HF returns dict-like or object) - pdb_str = None - coords = None - atom14_to_atom37, OFProtein, to_pdb = _get_pdb_utils() - - def _get_out(key: str): - if hasattr(out, key): - return getattr(out, key) - if isinstance(out, dict) and key in out: - return out[key] - return None - - positions = _get_out("positions") - if positions is not None: - try: - if atom14_to_atom37 and OFProtein is not None and to_pdb is not None: - # positions: often [num_recycling, batch, N, 14, 3]; use last recycling - pos = positions - if pos.dim() == 5: - pos = pos[-1] - final_atom37 = atom14_to_atom37(pos, out) - coords = final_atom37 - out_cpu = {} - for k in ("aatype", "atom37_atom_exists", "residue_index", "plddt", "chain_index"): - v = _get_out(k) - if v is not None: - out_cpu[k] = v.cpu().numpy() if isinstance(v, torch.Tensor) else v - pos_np = final_atom37.cpu().numpy() - for i in range(pos_np.shape[0]): - pred = OFProtein( - aatype=out_cpu["aatype"][i], - atom_positions=pos_np[i], - atom_mask=out_cpu["atom37_atom_exists"][i], - residue_index=out_cpu["residue_index"][i] + 1, - b_factors=out_cpu["plddt"][i], - chain_index=out_cpu.get("chain_index", [None] * pos_np.shape[0])[i] if "chain_index" in out_cpu else None, - ) - pdb_str = to_pdb(pred) - break - else: - pos = positions - if pos.dim() == 5: - pos = pos[-1, 0] - elif pos.dim() == 4: - pos = pos[0] - coords = pos - if coords.dim() == 4: - ca_idx = 1 - coords = coords[:, :, ca_idx, :] - pdb_str = _coords_to_minimal_pdb(coords[0], seq) - except Exception as e: - log(f"Warning: PDB conversion failed ({e}); writing minimal PDB if possible.") - if coords is not None: - try: - c = coords[0] if coords.dim() > 2 else coords - if c.dim() == 3: - c = c[:, 1, :] - pdb_str = _coords_to_minimal_pdb(c, seq) - except Exception: - pass - - if pdb_str is None and coords is not None: - c = coords[0] if coords.dim() > 2 else coords - if c.dim() == 3: - c = c[:, 1, :] - pdb_str = _coords_to_minimal_pdb(c, seq) - if pdb_str is None: - log("Warning: no PDB output from model; structure/ may be incomplete.") + log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " + f"{len(collector.activations)} activation layers.") + # Structure: PDB + optional coords tensor + pdb_str, coords = self._extract_structure(out, seq, log) struct_paths = write_structure(out_dir, pdb_str, coords) log(f"Structure written: {struct_paths}") @@ -287,16 +229,14 @@ def _get_out(key: str): except Exception: pass - layer_count = max( - len(collector.attention), - len(collector.activations), - 1, - ) + layer_count = max(len(collector.attention), len(collector.activations), 1) head_count = 0 if collector.attention: first_attn = next(iter(collector.attention.values())) - if first_attn.dim() >= 3: - head_count = first_attn.shape[-3] if first_attn.dim() == 3 else first_attn.shape[1] + if first_attn.dim() == 4: + head_count = first_attn.shape[1] + elif first_attn.dim() == 3: + head_count = first_attn.shape[0] build_and_write_meta( out_dir=out_dir, @@ -320,20 +260,91 @@ def _get_out(key: str): "structure": struct_paths, "out_dir": out_dir, "trace_mode": trace_mode, + "attention_layers": len(collector.attention), + "activation_layers": len(collector.activations), } + def _extract_structure(self, out: Any, seq: str, log) -> Tuple[Optional[str], Optional[torch.Tensor]]: + """Extract PDB string and coordinates from model output.""" + pdb_str = None + coords = None + atom14_to_atom37, OFProtein, to_pdb = _get_pdb_utils() + + def _get(key: str): + if hasattr(out, key): + return getattr(out, key) + if isinstance(out, dict) and key in out: + return out[key] + return None + + positions = _get("positions") + if positions is None: + log("Warning: no positions in model output; structure/ may be incomplete.") + return pdb_str, coords + + try: + if atom14_to_atom37 and OFProtein is not None and to_pdb is not None: + pos = positions[-1] if positions.dim() == 5 else positions + final_atom37 = atom14_to_atom37(pos, out) + coords = final_atom37 + out_cpu = {} + for k in ("aatype", "atom37_atom_exists", "residue_index", "plddt", "chain_index"): + v = _get(k) + if v is not None: + out_cpu[k] = v.cpu().numpy() if isinstance(v, torch.Tensor) else v + pos_np = final_atom37.cpu().numpy() + pred = OFProtein( + aatype=out_cpu["aatype"][0], + atom_positions=pos_np[0], + atom_mask=out_cpu["atom37_atom_exists"][0], + residue_index=out_cpu["residue_index"][0] + 1, + b_factors=out_cpu["plddt"][0], + chain_index=out_cpu.get("chain_index", [None])[0] if "chain_index" in out_cpu else None, + ) + pdb_str = to_pdb(pred) + else: + pos = positions + if pos.dim() == 5: + pos = pos[-1, 0] + elif pos.dim() == 4: + pos = pos[0] + coords = pos + pdb_str = _coords_to_minimal_pdb(coords, seq) + except Exception as e: + log(f"Warning: PDB conversion failed ({e}); writing minimal PDB.") + try: + if positions is not None: + pos = positions + if pos.dim() == 5: + pos = pos[-1, 0] + elif pos.dim() == 4: + pos = pos[0] + pdb_str = _coords_to_minimal_pdb(pos, seq) + coords = pos + except Exception: + pass + + if pdb_str is None and coords is not None: + pdb_str = _coords_to_minimal_pdb(coords, seq) + if pdb_str is None: + log("Warning: no PDB output from model; structure/ may be incomplete.") + + return pdb_str, coords + def _coords_to_minimal_pdb(coords: torch.Tensor, seq: str) -> str: - """Write minimal CA-only PDB from coords [N, 3] or [N, 37, 3].""" + """Write minimal CA-only PDB from coords [N, 14, 3] or [N, 37, 3] or [N, 3].""" if coords.dim() == 3: - ca = coords[:, 1, :] # assume atom37 order: N, CA, C, ... + ca = coords[:, 1, :] # atom37/atom14 order: N=0, CA=1, C=2, ... else: ca = coords lines = [] - for i in range(ca.shape[0]): - a = ca[i].cpu().numpy() - res = seq[i] if i < len(seq) else "X" + for i in range(min(ca.shape[0], len(seq))): + a = ca[i].float().cpu().numpy() + res3 = AA_1TO3.get(seq[i], "UNK") lines.append( - f"ATOM {i+1:5d} CA {res} A{i+1:4d} {a[0]:8.3f}{a[1]:8.3f}{a[2]:8.3f} 1.00 0.00 C" + f"ATOM {i+1:5d} CA {res3:>3s} A{i+1:4d} " + f"{a[0]:8.3f}{a[1]:8.3f}{a[2]:8.3f} 1.00 0.00 C" ) + lines.append("END") return "\n".join(lines) + "\n" diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py index d08b7fc7..01800687 100644 --- a/vizfold/backends/esmfold/trace_adapter.py +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -76,7 +76,9 @@ def write_traces( for key, t in collector.attention.items(): path = os.path.join(attn_dir, f"{key}.pt") if head_indices is not None and t.dim() >= 3: - t = t[:, head_indices, ...] + # Attention shape: [B, H, N, N] (4D) or [H, N, N] (3D) + head_dim = 1 if t.dim() == 4 else 0 + t = t.index_select(head_dim, torch.tensor(head_indices, device=t.device)) _save_tensor(path, t, save_fp16=save_fp16) attention_index[key] = { "path": path, @@ -120,8 +122,9 @@ def write_trace_summary( layer_key = f"{key}_slice{i}" if a.shape[0] > 1 else key block = a[i] if block.ndim >= 3: - # heads, N, N - ent = -np.sum(block * np.log(block + 1e-12), axis=(-2, -1)).mean() + # block: [heads, N, N] — each row is a distribution over keys + # Entropy per row (axis=-1), averaged over all rows and heads + ent = -np.sum(block * np.log(block + 1e-12), axis=-1).mean() summary["attention"][layer_key] = { "mean": float(block.mean()), "std": float(block.std()), From 1b29afb4077b7799f8429a7d048454e35f1823f3 Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Sun, 22 Mar 2026 21:47:02 -0400 Subject: [PATCH 05/18] Address PR #44 review: pin transformers, .DS_Store/.gitignore, trace relpaths, summary logging, layer_count --- .gitignore | 1 + docs/.DS_Store | Bin 6148 -> 0 bytes environment-mac.yml | 1 + environment.yml | 2 +- requirements-esmfold.txt | 2 +- vizfold/backends/esmfold/inference.py | 6 +++--- vizfold/backends/esmfold/trace_adapter.py | 4 ++-- 7 files changed, 9 insertions(+), 7 deletions(-) delete mode 100644 docs/.DS_Store diff --git a/.gitignore b/.gitignore index 3f1d8382..a36e08e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode/ .idea/ +.DS_Store __pycache__/ *.egg-info build diff --git a/docs/.DS_Store b/docs/.DS_Store deleted file mode 100644 index f1054d31e643e516df4b6bf2dfdde14e3d393721..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%SyvQ6rE|KO({Ya3SADkE!Y=|xCycTfDv7&)P$58Of#iP?V=R2)*tdq{2uR} znTW-@6|wii%(>5*%z?~l-IVu|j-5Wy{lZ?o5jBt@n!$gK)znR!y z2mE%6#caaHEc^QX;WUY}yxo51m8!L~TeE9+!@l<)<-*T{e4cs1>=vynDU-0&gYY^Y zEr!nixlHpQPDe9U5Jw|Oxx0zeNG?1%PoqrL`Z{3O?fTGZE|;C&NlSG5omERLPrL1w z==Y9StGaz~cyx9-d`c#%e9?q*;9JR_!4h6U`CQMlKT8vtj=@*vmwAN505L!e5ChxG zfH@QF#`aP`D<=ksfgc#a{XsxObPX07)z$$WUY{{;A)<=_`4&ox+R)a8t;nPD6=bNP7TYIg7omCm@Uk$Pf)7+7bZs!a#a{|oqKHa_y# zOUNPyh=G5`0JjGIz=K7Zv-R8Z@T?Wk9-yIMUV#b-=xdh%FmNAfs-TVw)FIC`SZKsi T(67n?=^~&Ap^g~%1qMC=4.36.0,<5.0.0 - pip: - dm-tree==0.1.6 - einops # required by fair-esm/ESMFold diff --git a/environment.yml b/environment.yml index 55fcbdeb..29e6a836 100644 --- a/environment.yml +++ b/environment.yml @@ -39,4 +39,4 @@ dependencies: - dm-tree==0.1.6 - git+https://github.com/NVIDIA/dllogger.git - flash-attn==2.6.3 - - transformers # ESMFold backend (run_pretrained_esmf.py), no OpenFold build + - transformers>=4.36.0,<5.0.0 diff --git a/requirements-esmfold.txt b/requirements-esmfold.txt index 22dc05b1..5dfcc16c 100644 --- a/requirements-esmfold.txt +++ b/requirements-esmfold.txt @@ -1,4 +1,4 @@ # ESMFold backend (run_pretrained_esmf.py). Install after PyTorch. # Uses HuggingFace Transformers to avoid OpenFold build dependency. # Use: pip install torch && pip install -r requirements-esmfold.txt -transformers>=4.36.0 +transformers>=4.36.0,<5.0.0 diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 507d3179..d776f79b 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -226,10 +226,10 @@ def log(msg: str) -> None: shapes_recorded["activations"][k] = v.get("shape", []) try: write_trace_summary(out_dir, collector) - except Exception: - pass + except Exception as e: + log(f"Warning: trace summary failed: {e}") - layer_count = max(len(collector.attention), len(collector.activations), 1) + layer_count = len(collector.attention) if trace_mode != "none" else 0 head_count = 0 if collector.attention: first_attn = next(iter(collector.attention.values())) diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py index 01800687..287e3753 100644 --- a/vizfold/backends/esmfold/trace_adapter.py +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -81,7 +81,7 @@ def write_traces( t = t.index_select(head_dim, torch.tensor(head_indices, device=t.device)) _save_tensor(path, t, save_fp16=save_fp16) attention_index[key] = { - "path": path, + "path": os.path.relpath(path, out_dir), "dtype": str(t.dtype), "shape": list(t.shape), } @@ -90,7 +90,7 @@ def write_traces( path = os.path.join(act_dir, f"{key}.pt") _save_tensor(path, t, save_fp16=save_fp16) activations_index[key] = { - "path": path, + "path": os.path.relpath(path, out_dir), "dtype": str(t.dtype), "shape": list(t.shape), } From aed32157dbf29d938ff1bbe0de3c5f52cf59951c Mon Sep 17 00:00:00 2001 From: rohan5986 <136632833+rohan5986@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:04:35 -0400 Subject: [PATCH 06/18] Extract s_s folding trunk activations and enforce safetensors (#2) Co-authored-by: Rohan Singhal --- vizfold/backends/esmfold/inference.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index d776f79b..b8f44f27 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -123,7 +123,7 @@ def load_model(self) -> Any: warnings.warn("Deterministic mode may reduce speed.", UserWarning) self._tokenizer = AutoTokenizer.from_pretrained(self.model_name) - self._model = EsmForProteinFolding.from_pretrained(self.model_name) + self._model = EsmForProteinFolding.from_pretrained(self.model_name, use_safetensors=True) self._model = self._model.eval() dtype_t = torch.float16 if self.dtype == "float16" else torch.float32 self._model = self._model.to(device=self.device, dtype=dtype_t) @@ -202,6 +202,15 @@ def log(msg: str) -> None: if trace_mode != "none": collector.remove_hooks() + # Check if the output object contains the single representations + if hasattr(out, 's_s') and out.s_s is not None: + # Move to CPU and remove the batch dimension -> [seq_len, hidden_dim] + single_reps = out.s_s.squeeze(0).cpu() + + log(f"Extracted folding trunk s_s activations: {single_reps.shape}") + else: + log("Warning: out.s_s not found. Folding trunk single representations missing.") + log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " f"{len(collector.activations)} activation layers.") From ca21eb5833a8109c24dc9305e6a7241ae4cd5c0a Mon Sep 17 00:00:00 2001 From: Jeeva Ramasamy <94267209+JeevanandanRamasamy@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:12:54 -0400 Subject: [PATCH 07/18] Add VizFold-compatible text attention export to HuggingFace backend (#1) * Add VizFold text-file attention export compatible with existing visualization tools * Bug fix: override the positional arg in-place instead of adding to kwargs * Fix: trace_formats missing from meta.json * Robust attention saving & forward signature handling hooks.py: - make the EsmSelfAttention forward patch resilient to signature changes by finding the position of output_attentions by name instead of assuming a fixed positional index trace_adapter.py: - reuse OpenFold's save_attention_topk if available, and falls back to a self-contained NumPy implementation (no OpenFold dependency) that writes msa_row_attn text files - layer-index extraction via regex - compute produced trace_formats dynamically in build_and_write_meta instead of hardcoding ["pt","txt"] --- run_pretrained_esmf.py | 7 ++ vizfold/backends/esmfold/hooks.py | 11 ++- vizfold/backends/esmfold/inference.py | 11 +++ vizfold/backends/esmfold/schema.py | 3 + vizfold/backends/esmfold/trace_adapter.py | 96 ++++++++++++++++++++++- 5 files changed, 122 insertions(+), 6 deletions(-) diff --git a/run_pretrained_esmf.py b/run_pretrained_esmf.py index 1be4c0b3..73e61884 100644 --- a/run_pretrained_esmf.py +++ b/run_pretrained_esmf.py @@ -92,6 +92,12 @@ def main() -> int: action="store_true", help="Use deterministic CuDNN (may reduce speed).", ) + parser.add_argument( + "--top_k", + type=int, + default=50, + help="Number of top attention values to save per head in VizFold text files.", + ) args = parser.parse_args() if not os.path.isfile(args.fasta): @@ -125,6 +131,7 @@ def main() -> int: layers=args.layers, heads=args.heads, save_fp16=args.save_fp16, + top_k=args.top_k, ) print(f"Done. Outputs in {args.out}") print(f" attention layers: {result.get('attention_layers', 0)}, " diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index d8b9f15d..3879fca2 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple import torch +import inspect import torch.nn as nn @@ -122,11 +123,15 @@ def _patch_and_hook_attention(self, self_attn: nn.Module, layer_idx: int) -> Non directly to get the full tuple. """ orig_forward = self_attn.forward + params = list(inspect.signature(orig_forward).parameters) + # Find output_attentions position by name — robust to signature changes + oa_pos = params.index("output_attentions") if "output_attentions" in params else -1 def patched_forward(*args, **kwargs): - # Force output_attentions so the attention weights are computed - # and included in the return tuple. - kwargs["output_attentions"] = True + if oa_pos >= 0 and oa_pos < len(args): + args = args[:oa_pos] + (True,) + args[oa_pos + 1:] + else: + kwargs["output_attentions"] = True return orig_forward(*args, **kwargs) self_attn.forward = patched_forward diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index b8f44f27..2c24c892 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -17,6 +17,7 @@ write_structure, write_traces, write_trace_summary, + write_attention_txt, ) # HuggingFace ESMFold @@ -137,6 +138,7 @@ def run( layers: Optional[str] = None, heads: Optional[str] = None, save_fp16: bool = False, + top_k: int = 50, log_path: Optional[str] = None, ) -> Dict[str, Any]: """ @@ -238,6 +240,14 @@ def log(msg: str) -> None: except Exception as e: log(f"Warning: trace summary failed: {e}") + # VizFold-compatible text-file attention export + if want_attn and collector.attention: + txt_dir = write_attention_txt(out_dir, collector, top_k=top_k) + if txt_dir: + attn_files = [f for f in os.listdir(txt_dir) if f.endswith(".txt")] + shapes_recorded["attention_files"] = attn_files + log(f"VizFold text attention saved to {txt_dir} ({len(attn_files)} files)") + layer_count = len(collector.attention) if trace_mode != "none" else 0 head_count = 0 if collector.attention: @@ -262,6 +272,7 @@ def log(msg: str) -> None: seed=self.seed, deterministic=self.deterministic, save_fp16=save_fp16, + top_k=top_k, ) log("meta.json written.") diff --git a/vizfold/backends/esmfold/schema.py b/vizfold/backends/esmfold/schema.py index 5a934ec7..626bcffc 100644 --- a/vizfold/backends/esmfold/schema.py +++ b/vizfold/backends/esmfold/schema.py @@ -49,6 +49,7 @@ def build_meta( seed: Optional[int] = None, deterministic: bool = False, save_fp16: bool = False, + top_k: int = 50, repo_path: Optional[str] = None, ) -> Dict[str, Any]: """ @@ -67,7 +68,9 @@ def build_meta( "layer_count": layer_count, "head_count": head_count, "trace_mode": trace_mode, + "trace_formats": trace_formats, "tensor_format": "fp16" if save_fp16 else "fp32", + "top_k": top_k, "shapes_recorded": shapes_recorded, } if fasta_path: diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py index 287e3753..fa8c317d 100644 --- a/vizfold/backends/esmfold/trace_adapter.py +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -15,9 +15,13 @@ layer_000.pt ... index.json + attention_files/ + msa_row_attn_layer0.txt (VizFold text format) + ... logs.txt (caller appends) """ import os +import re from typing import Any, Dict, Optional, Tuple import torch @@ -99,6 +103,81 @@ def write_traces( return attention_index, activations_index +def write_attention_txt( + out_dir: str, + collector: ESMFoldTraceCollector, + top_k: int = 50, +) -> Optional[str]: + """ + Write VizFold-compatible text-file attention maps from collector. + + Converts each [B, H, N, N] attention tensor into the standard + msa_row_attn_layer*.txt format used by VizFold visualization tools + (PyMOL scripts, arc diagrams, etc.). + + Does not require OpenFold to be installed — uses a self-contained + implementation of the top-k writing logic with numpy only. + + Args: + out_dir: Root output directory. Files go to out_dir/attention_files/. + collector: ESMFoldTraceCollector with populated .attention dict. + top_k: Number of top attention values to save per head. + + Returns: + Path to the attention_files directory, or None if no attention data. + """ + import numpy as np + + if not collector.attention: + return None + + attn_dir = os.path.join(out_dir, "attention_files") + os.makedirs(attn_dir, exist_ok=True) + + # Try to use OpenFold's implementation for format consistency; + # fall back to the self-contained version if openfold is not installed. + try: + from openfold.model.evoformer import save_attention_topk as _of_save + + def _save(arr: "np.ndarray", layer_idx: int) -> None: + key = f"layer_{layer_idx:03d}" + _of_save( + attention_dict={key: arr}, + save_dir=attn_dir, + layer_name=key, + layer_idx=layer_idx, + attn_type="msa_row_attn", + triangle_residue_idx=None, + top_k=top_k, + ) + + except ImportError: + # Standalone implementation — same output format as save_attention_topk. + # arr shape: [B, H, N, N]; msa_row_attn slices batch dim → [H, N, N]. + def _save(arr: "np.ndarray", layer_idx: int) -> None: # type: ignore[misc] + heads = arr[0] # [H, N, N] + path = os.path.join(attn_dir, f"msa_row_attn_layer{layer_idx}.txt") + with open(path, "w") as f: + for head_idx, attn_map in enumerate(heads): + S = attn_map.shape[0] + k = min(top_k, S * S) + flat = np.argsort(attn_map.flatten())[::-1][:k] + rows, cols = np.unravel_index(flat, (S, S)) + scores = attn_map[rows, cols] + f.write(f"Layer {layer_idx}, Head {head_idx}\n") + for r, c, s in zip(rows, cols, scores): + f.write(f"{r} {c} {s:.6f}\n") + print(f"[Done] Saved top {top_k} entries for msa_row_attn to {path}") + + for key, t in collector.attention.items(): + m = re.search(r"\d+", key) + layer_idx = int(m.group()) if m else 0 + arr = t.float().cpu().numpy() + _save(arr, layer_idx) + + return attn_dir + + def write_trace_summary( out_dir: str, collector: "ESMFoldTraceCollector", @@ -128,9 +207,9 @@ def write_trace_summary( summary["attention"][layer_key] = { "mean": float(block.mean()), "std": float(block.std()), - "entropy_proxy": float(ent), + "entropy_proxy": float(ent), "sparsity_proxy": float((block < 1e-5).mean()), - } + } for key, t in collector.activations.items(): h = t.float().cpu().numpy() if h.size == 0: @@ -162,7 +241,17 @@ def build_and_write_meta( seed: Optional[int] = None, deterministic: bool = False, save_fp16: bool = False, + top_k: int = 50, ) -> str: + # Determine which formats were actually produced + formats = [] + if os.path.isdir(os.path.join(out_dir, "trace", "attention")): + formats.append("pt") + if os.path.isdir(os.path.join(out_dir, "attention_files")): + formats.append("txt") + if not formats: + formats = ["none"] + meta = build_meta( backend="esmfold", model_name=model_name, @@ -175,10 +264,11 @@ def build_and_write_meta( layer_count=layer_count, head_count=head_count, trace_mode=trace_mode, - trace_formats=["pt"], # VizFold trace format + trace_formats=formats, shapes_recorded=shapes_recorded, seed=seed, deterministic=deterministic, save_fp16=save_fp16, + top_k=top_k, ) return write_meta(meta, out_dir) From fb777c9003d00f216e4218547773b7c4eb920e2f Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Mon, 23 Mar 2026 21:43:42 -0400 Subject: [PATCH 08/18] Gate s_s extraction under trace_mode and persist to trace archive --- vizfold/backends/esmfold/inference.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 2c24c892..37b1364e 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -201,17 +201,15 @@ def log(msg: str) -> None: with torch.no_grad(): out = model(input_ids=input_ids, attention_mask=attention_mask) + single_reps = None if trace_mode != "none": collector.remove_hooks() - # Check if the output object contains the single representations - if hasattr(out, 's_s') and out.s_s is not None: - # Move to CPU and remove the batch dimension -> [seq_len, hidden_dim] - single_reps = out.s_s.squeeze(0).cpu() - - log(f"Extracted folding trunk s_s activations: {single_reps.shape}") - else: - log("Warning: out.s_s not found. Folding trunk single representations missing.") + if hasattr(out, 's_s') and out.s_s is not None: + single_reps = out.s_s.squeeze(0).cpu() + log(f"[{self.model_name}] [{trace_mode}] Extracted folding trunk s_s: {single_reps.shape}") + else: + log(f"[{self.model_name}] [{trace_mode}] out.s_s not found — folding trunk single representations missing.") log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " f"{len(collector.activations)} activation layers.") @@ -235,6 +233,16 @@ def log(msg: str) -> None: shapes_recorded["attention"][k] = v.get("shape", []) for k, v in act_idx.items(): shapes_recorded["activations"][k] = v.get("shape", []) + if single_reps is not None: + s_s_path = os.path.join(out_dir, "trace", "activations", "folding_trunk_s_s.pt") + torch.save(single_reps if not save_fp16 else single_reps.half(), s_s_path) + shapes_recorded["activations"]["folding_trunk_s_s"] = { + "path": os.path.relpath(s_s_path, out_dir), + "dtype": str(single_reps.dtype), + "shape": list(single_reps.shape), + } + log(f"Folding trunk s_s written to {s_s_path}") + try: write_trace_summary(out_dir, collector) except Exception as e: From 4b86182cf7cd4acd882ac659e3679a78e398bb36 Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Mon, 23 Mar 2026 21:43:48 -0400 Subject: [PATCH 09/18] Add trace extraction smoke test and reproducibility docs Co-authored-by: Mose Kim --- docs/esmfold_backend_repro.md | 156 ++++++++++++++++++++++++++++++++++ tests/test_esmf_smoke.py | 58 ++++++++++++- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 docs/esmfold_backend_repro.md diff --git a/docs/esmfold_backend_repro.md b/docs/esmfold_backend_repro.md new file mode 100644 index 00000000..00f3a788 --- /dev/null +++ b/docs/esmfold_backend_repro.md @@ -0,0 +1,156 @@ +# ESMFold Backend Reproducibility Guide + +This document describes how to run the HuggingFace-based ESMFold backend with +VizFold-compatible trace export using the shared `feature/esmfold-backend` +integration branch. + +It provides instructions for verifying structure inference, attention extraction, +activation extraction, and expected archive outputs locally and on the ICE cluster. + +## Environment Setup (Local) + +Create a virtual environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Install dependencies: + +```bash +pip install -r requirements-esmfold.txt +pip install torch +pip install -e . --no-build-isolation +``` + +## Structure-Only Inference Test + +Run: + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/test_run \ + --trace_mode none \ + --device cpu +``` + +Expected outputs: + +- `outputs/test_run/meta.json` +- `outputs/test_run/structure/predicted.pdb` + +Successful execution confirms: + +- model loads correctly +- inference pipeline runs end-to-end +- archive metadata generation works + +## Trace Extraction Test (Attention + Activations) + +Run: + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/test_trace \ + --trace_mode attention+activations \ + --device cpu +``` + +Expected outputs: + +- `outputs/test_trace/meta.json` +- `outputs/test_trace/structure/predicted.pdb` +- `outputs/test_trace/trace/` + +Trace directory should contain: + +- `trace/attention/` +- `trace/activations/` + +## Verified Tensor Outputs (Local Validation) + +Successful execution produces: + +- 36 attention tensors +- 36 activation tensors +- 72 total `.pt` trace tensors + +Attention tensors follow expected shape: + +`[B, H, N, N]` + +where: + +- B = batch size +- H = number of attention heads +- N = sequence length (after special-token slicing) + +This confirms compatibility with VizFold's visualization pipeline. + +## Archive Structure Validation + +Expected archive layout: + +``` +outputs/test_trace/ +├── meta.json +├── structure/ +│ └── predicted.pdb +└── trace/ + ├── attention/ + └── activations/ +``` + +This structure matches the OpenFold-compatible VizFold archive schema. + +## Running on ICE Cluster (PACE) + +Login: + +```bash +ssh @login-ice.pace.gatech.edu +``` + +Navigate to the repository: + +```bash +cd attention-viz-demo +``` + +Activate environment: + +```bash +source .venv/bin/activate +``` + +Run structure inference: + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/test_run \ + --trace_mode none \ + --device cuda +``` + +Run trace extraction: + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/test_trace \ + --trace_mode attention+activations \ + --device cuda +``` + +Expected outputs: + +- `structure/predicted.pdb` +- `meta.json` +- `trace/attention/` +- `trace/activations/` + +GPU execution confirms cluster compatibility for larger inference workloads. diff --git a/tests/test_esmf_smoke.py b/tests/test_esmf_smoke.py index 6d2eabfc..23b06756 100644 --- a/tests/test_esmf_smoke.py +++ b/tests/test_esmf_smoke.py @@ -120,16 +120,64 @@ def test_esmf_smoke_run_cpu(tmp_path=None): assert (out_dir / "meta.json").exists() assert (out_dir / "logs.txt").exists() - # Structure may or may not exist depending on model output structure_dir = out_dir / "structure" if structure_dir.exists(): assert list(structure_dir.iterdir()) +def test_esmf_trace_extraction(tmp_path=None): + """ + Run ESMFold with attention+activations, verify trace tensors are + written and have the expected shape. + """ + if not _has_esm(): + return + import torch + + if tmp_path is None: + tmp_path = Path(tempfile.mkdtemp()) + fasta = tmp_path / "tiny.fasta" + fasta.write_text(">tiny\nMKFLKFSL\n") + out_dir = tmp_path / "out_trace" + out_dir.mkdir(parents=True, exist_ok=True) + + result = subprocess.run( + [ + sys.executable, + str(REPO_ROOT / "run_pretrained_esmf.py"), + "--fasta", str(fasta), + "--out", str(out_dir), + "--device", "cpu", + "--trace_mode", "attention+activations", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=600, + ) + assert result.returncode == 0, (result.stdout, result.stderr) + + assert (out_dir / "meta.json").exists() + assert (out_dir / "structure" / "predicted.pdb").exists() + trace_dir = out_dir / "trace" + assert trace_dir.exists() + + pt_files = list(trace_dir.rglob("*.pt")) + assert len(pt_files) >= 36, f"Expected >=36 trace tensors, got {len(pt_files)}" + + attn_files = sorted((trace_dir / "attention").glob("*.pt")) + assert attn_files, "No attention .pt files found" + sample = torch.load(attn_files[0], map_location="cpu", weights_only=True) + assert sample.dim() == 4, f"Expected 4D attention tensor [B,H,N,N], got {sample.shape}" + assert sample.shape[0] == 1, f"Expected batch dim == 1, got {sample.shape[0]}" + assert sample.shape[2] == sample.shape[3], f"Attention map not square: {sample.shape}" + + # Pytest decorators when available if pytest is not None: test_esmf_import = pytest.mark.skipif(not _has_esm(), reason="transformers not installed")(test_esmf_import) test_esmf_smoke_run_cpu = pytest.mark.skipif(not _has_esm(), reason="transformers not installed")(test_esmf_smoke_run_cpu) + test_esmf_trace_extraction = pytest.mark.skipif(not _has_esm(), reason="transformers not installed")(test_esmf_trace_extraction) if __name__ == "__main__": @@ -182,6 +230,14 @@ def test_esmf_smoke_run_cpu(tmp_path=None): except Exception as e: print("FAIL", e) failed.append("test_esmf_smoke_run_cpu") + # Trace extraction + print("test_esmf_trace_extraction ...", end=" ") + try: + test_esmf_trace_extraction() + print("ok (or skipped)") + except Exception as e: + print("FAIL", e) + failed.append("test_esmf_trace_extraction") if failed: print("Failed:", failed) sys.exit(1) From a3a9ac5bbcfcbba735d48bb2edf1a6213233f3b2 Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Wed, 25 Mar 2026 22:06:59 -0400 Subject: [PATCH 10/18] Document s_s extraction and structure PDB fallback chain --- vizfold/backends/esmfold/inference.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 37b1364e..62c0dbae 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -205,6 +205,9 @@ def log(msg: str) -> None: if trace_mode != "none": collector.remove_hooks() + # out.s_s: folding trunk single representations [B, N, 1024]. + # These are the per-residue embeddings produced by ESMFold's + # structure module which complements the ESM-2 encoder traces. if hasattr(out, 's_s') and out.s_s is not None: single_reps = out.s_s.squeeze(0).cpu() log(f"[{self.model_name}] [{trace_mode}] Extracted folding trunk s_s: {single_reps.shape}") @@ -310,8 +313,13 @@ def _get(key: str): log("Warning: no positions in model output; structure/ may be incomplete.") return pdb_str, coords + # Fallback chain: full OpenFold PDB conversion → CA-only minimal PDB. + # When transformers bundles openfold_utils we get proper all-atom PDB; + # otherwise we write a CA-only PDB so downstream tools still have geometry. try: if atom14_to_atom37 and OFProtein is not None and to_pdb is not None: + # ESMFold returns positions as [recycling_iters, B, N, 14, 3]; + # take the last iteration for the final refined structure. pos = positions[-1] if positions.dim() == 5 else positions final_atom37 = atom14_to_atom37(pos, out) coords = final_atom37 From db6caba6c4c7292e4f4b3ff747acffa5118756d5 Mon Sep 17 00:00:00 2001 From: jayvenn21 Date: Thu, 26 Mar 2026 21:00:59 -0400 Subject: [PATCH 11/18] Add structure module hooks for IPA attention and per-recycle backbone capture --- run_pretrained_esmf.py | 6 ++ vizfold/backends/esmfold/hooks.py | 135 ++++++++++++++++++++++++++ vizfold/backends/esmfold/inference.py | 60 +++++++++++- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/run_pretrained_esmf.py b/run_pretrained_esmf.py index 73e61884..92a2adb8 100644 --- a/run_pretrained_esmf.py +++ b/run_pretrained_esmf.py @@ -98,6 +98,11 @@ def main() -> int: default=50, help="Number of top attention values to save per head in VizFold text files.", ) + parser.add_argument( + "--structure_traces", + action="store_true", + help="Capture IPA attention weights and per-recycle backbone positions from the structure module.", + ) args = parser.parse_args() if not os.path.isfile(args.fasta): @@ -132,6 +137,7 @@ def main() -> int: heads=args.heads, save_fp16=args.save_fp16, top_k=args.top_k, + structure_traces=args.structure_traces, ) print(f"Done. Outputs in {args.out}") print(f" attention layers: {result.get('attention_layers', 0)}, " diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index 3879fca2..43febde2 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -11,6 +11,12 @@ The ESM-2 tokenizer adds and tokens, so attention maps are (seq_len+2, seq_len+2). We slice out the special tokens so stored tensors are (seq_len, seq_len), matching the FASTA sequence. + +Structure module tracing: + StructureModuleTraceCollector hooks into model.trunk.structure_module.ipa + to capture IPA attention weights [H, N, N] at each block within each + recycling iteration, and hooks on trunk.structure_module to capture + per-recycle backbone positions and single representations. """ import re import warnings @@ -164,3 +170,132 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: key = f"layer_{layer_idx:03d}" self.activations[key] = h.detach() return hook + + +class StructureModuleTraceCollector: + """ + Captures IPA attention weights and per-recycling-iteration backbone + outputs from the ESMFold structure module. + + Hook targets: + - trunk.structure_module.ipa: monkey-patched to stash the softmax + attention matrix a [*, H, N, N] before it's consumed internally. + Fires num_blocks times per recycle (IPA is a single shared module + reused across all structure module blocks). + - trunk.structure_module: captures the full output dict per recycle, + including stacked positions [num_blocks, B, N, 14, 3], frames, + and the final single representation. + """ + + def __init__(self) -> None: + self.ipa_attention: Dict[str, torch.Tensor] = {} + self.backbone_positions: Dict[str, torch.Tensor] = {} + self.backbone_frames: Dict[str, torch.Tensor] = {} + self.sm_states: Dict[str, torch.Tensor] = {} + self._handles: List[Any] = [] + self._patched_forwards: List[Tuple[nn.Module, Callable]] = [] + self._recycle_idx = 0 + self._block_idx = 0 + + def clear(self) -> None: + self.ipa_attention.clear() + self.backbone_positions.clear() + self.backbone_frames.clear() + self.sm_states.clear() + self._recycle_idx = 0 + self._block_idx = 0 + + def register_hooks(self, model: nn.Module) -> None: + """ + Register hooks on model.trunk.structure_module and its IPA submodule. + + Args: + model: the full EsmForProteinFolding model (we navigate to trunk). + """ + trunk = getattr(model, "trunk", None) + if trunk is None: + warnings.warn("model.trunk not found; structure module hooks skipped.", UserWarning) + return + sm = getattr(trunk, "structure_module", None) + if sm is None: + warnings.warn("trunk.structure_module not found; hooks skipped.", UserWarning) + return + + ipa = getattr(sm, "ipa", None) + if ipa is not None: + self._patch_ipa(ipa) + + h = sm.register_forward_hook(self._sm_output_hook) + self._handles.append(h) + + def _patch_ipa(self, ipa: nn.Module) -> None: + """ + Monkey-patch IPA forward to stash the softmax attention matrix. + + IPA computes a = softmax(scalar_attn + point_attn + pair_bias + mask) + but only returns the single-rep update. We intercept a after softmax + and store it, keyed by recycle_idx and block_idx. + + ipa.softmax is an nn.Softmax module, so we can't replace it with a + plain function (PyTorch __setattr__ rejects non-Module assignments). + Instead we wrap it in an nn.Module subclass that captures the output. + """ + orig_forward = ipa.forward + orig_softmax_module = ipa.softmax + collector = self + + class CapturingSoftmax(nn.Module): + def __init__(self, wrapped: nn.Module): + super().__init__() + self.wrapped = wrapped + self.last_a: Optional[torch.Tensor] = None + + def forward(self, x: torch.Tensor) -> torch.Tensor: + result = self.wrapped(x) + self.last_a = result.detach() + return result + + capturing = CapturingSoftmax(orig_softmax_module) + ipa.softmax = capturing + + def patched_forward(*args, **kwargs): + capturing.last_a = None + out = orig_forward(*args, **kwargs) + if capturing.last_a is not None: + key = f"recycle_{collector._recycle_idx:02d}_block_{collector._block_idx:02d}" + collector.ipa_attention[key] = capturing.last_a + collector._block_idx += 1 + return out + + ipa.forward = patched_forward + self._patched_forwards.append((ipa, orig_forward)) + self._orig_softmax = (ipa, orig_softmax_module) + + def _sm_output_hook(self, module: nn.Module, inp: Any, out: Any) -> None: + """ + Fires once per recycling iteration after the full structure module + forward (all blocks). Captures backbone positions and states. + """ + key = f"recycle_{self._recycle_idx:02d}" + if isinstance(out, dict): + if "positions" in out: + self.backbone_positions[key] = out["positions"].detach().cpu() + if "frames" in out: + self.backbone_frames[key] = out["frames"].detach().cpu() + if "single" in out: + self.sm_states[key] = out["single"].detach().cpu() + + self._recycle_idx += 1 + self._block_idx = 0 + + def remove_hooks(self) -> None: + for h in self._handles: + h.remove() + self._handles.clear() + for module, orig_forward in self._patched_forwards: + module.forward = orig_forward + self._patched_forwards.clear() + if hasattr(self, "_orig_softmax"): + ipa_mod, orig_sm = self._orig_softmax + ipa_mod.softmax = orig_sm + del self._orig_softmax diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 62c0dbae..c5c2480e 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -10,7 +10,7 @@ import torch -from vizfold.backends.esmfold.hooks import ESMFoldTraceCollector +from vizfold.backends.esmfold.hooks import ESMFoldTraceCollector, StructureModuleTraceCollector from vizfold.backends.esmfold.schema import _read_fasta_and_hash from vizfold.backends.esmfold.trace_adapter import ( build_and_write_meta, @@ -139,6 +139,7 @@ def run( heads: Optional[str] = None, save_fp16: bool = False, top_k: int = 50, + structure_traces: bool = False, log_path: Optional[str] = None, ) -> Dict[str, Any]: """ @@ -147,6 +148,8 @@ def run( trace_mode: "attention" | "activations" | "attention+activations" | "none" layers: "all" or "0,1,2" or "0:12" heads: "all" or "0,1,2" + structure_traces: if True, capture IPA attention weights and per-recycle + backbone positions from the folding trunk structure module. """ os.makedirs(out_dir, exist_ok=True) if log_path is None: @@ -192,12 +195,18 @@ def log(msg: str) -> None: if attention_mask is not None: attention_mask = attention_mask.to(device=self.device) - # HF EsmForProteinFolding does NOT accept output_attentions/output_hidden_states. - # Hooks on model.esm capture traces during the single forward pass. + # Hooks on model.esm capture ESM-2 encoder traces during forward. if trace_mode != "none": esm_trunk = getattr(model, "esm", model) collector.register_hooks(esm_trunk) + # Structure module hooks: IPA attention + per-recycle backbone + sm_collector = None + if structure_traces: + sm_collector = StructureModuleTraceCollector() + sm_collector.register_hooks(model) + log("Structure module hooks registered (IPA attention + backbone).") + with torch.no_grad(): out = model(input_ids=input_ids, attention_mask=attention_mask) @@ -207,13 +216,18 @@ def log(msg: str) -> None: # out.s_s: folding trunk single representations [B, N, 1024]. # These are the per-residue embeddings produced by ESMFold's - # structure module which complements the ESM-2 encoder traces. + # structure module, complementing the ESM-2 encoder traces. if hasattr(out, 's_s') and out.s_s is not None: single_reps = out.s_s.squeeze(0).cpu() log(f"[{self.model_name}] [{trace_mode}] Extracted folding trunk s_s: {single_reps.shape}") else: log(f"[{self.model_name}] [{trace_mode}] out.s_s not found — folding trunk single representations missing.") + if sm_collector is not None: + sm_collector.remove_hooks() + log(f"Structure module traces: {len(sm_collector.ipa_attention)} IPA blocks, " + f"{len(sm_collector.backbone_positions)} recycle iterations.") + log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " f"{len(collector.activations)} activation layers.") @@ -259,6 +273,44 @@ def log(msg: str) -> None: shapes_recorded["attention_files"] = attn_files log(f"VizFold text attention saved to {txt_dir} ({len(attn_files)} files)") + # Write structure module traces (IPA attention + backbone per recycle) + if sm_collector is not None: + sm_dir = os.path.join(out_dir, "trace", "structure_module") + ipa_dir = os.path.join(sm_dir, "ipa_attention") + bb_dir = os.path.join(sm_dir, "backbone") + os.makedirs(ipa_dir, exist_ok=True) + os.makedirs(bb_dir, exist_ok=True) + + shapes_recorded["structure_module"] = {"ipa_attention": {}, "backbone": {}} + + for key, t in sm_collector.ipa_attention.items(): + path = os.path.join(ipa_dir, f"{key}.pt") + torch.save(t.cpu() if not save_fp16 else t.cpu().half(), path) + shapes_recorded["structure_module"]["ipa_attention"][key] = { + "path": os.path.relpath(path, out_dir), + "shape": list(t.shape), + } + + for key, t in sm_collector.backbone_positions.items(): + path = os.path.join(bb_dir, f"{key}_positions.pt") + torch.save(t if not save_fp16 else t.half(), path) + shapes_recorded["structure_module"]["backbone"][f"{key}_positions"] = { + "path": os.path.relpath(path, out_dir), + "shape": list(t.shape), + } + + for key, t in sm_collector.sm_states.items(): + path = os.path.join(bb_dir, f"{key}_states.pt") + torch.save(t if not save_fp16 else t.half(), path) + shapes_recorded["structure_module"]["backbone"][f"{key}_states"] = { + "path": os.path.relpath(path, out_dir), + "shape": list(t.shape), + } + + log(f"Structure module traces written: " + f"{len(sm_collector.ipa_attention)} IPA attention maps, " + f"{len(sm_collector.backbone_positions)} backbone snapshots.") + layer_count = len(collector.attention) if trace_mode != "none" else 0 head_count = 0 if collector.attention: From eebb1af1a388e3c092734a79101d8778f1c7073e Mon Sep 17 00:00:00 2001 From: rohan5986 <136632833+rohan5986@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:04:19 -0400 Subject: [PATCH 12/18] Capture s_s and s_z at every recycling iteration via trunk hook (#3) * Extract s_s folding trunk activations and enforce safetensors * Update backend pipeline * Capture s_s and s_z at every recycling iteration via trunk hook * Remove test output artifacts --------- Co-authored-by: Rohan Singhal --- run_test.py | 15 +++++++ test_trace.py | 65 +++++++++++++++++++++++++++ vizfold/backends/esmfold/hooks.py | 24 ++++++++++ vizfold/backends/esmfold/inference.py | 12 +++++ 4 files changed, 116 insertions(+) create mode 100644 run_test.py create mode 100644 test_trace.py diff --git a/run_test.py b/run_test.py new file mode 100644 index 00000000..d7854868 --- /dev/null +++ b/run_test.py @@ -0,0 +1,15 @@ +from vizfold.backends.esmfold.inference import ESMFoldRunner + +print("Loading ESMFoldRunner...") +# Initialize the lead's class +runner = ESMFoldRunner(device="cpu") + +print("Running inference...") +# Run the forward pass on our test fasta +results = runner.run( + fasta_path="test.fasta", + out_dir="test_output", + trace_mode="attention+activations" +) + +print(f"\nSuccess! Output saved to: {results['out_dir']}") \ No newline at end of file diff --git a/test_trace.py b/test_trace.py new file mode 100644 index 00000000..9e52d57e --- /dev/null +++ b/test_trace.py @@ -0,0 +1,65 @@ +import torch +import sys + +def validate_trace(file_path): + print(f"Loading trace from: {file_path}") + + # Load the output dictionary + if file_path.endswith('.pt'): + data = torch.load(file_path, weights_only=True) + else: + import pickle + with open(file_path, 'rb') as f: + data = pickle.load(f) + + seq = data.get('sequence', None) + if not seq: + print("Could not find 'sequence' string in the trace output.") + return + + N = len(seq) + print(f"\nSequence: {seq}") + print(f"Target Sequence Length (N): {N}") + print("-" * 40) + + # 1. Check Attention Slicing + attentions = data.get('attention', data.get('attentions')) + if attentions is not None: + # Check if the lead saved it as a list of tensors instead of one big tensor + if isinstance(attentions, list): + print(f"Structure: List of {len(attentions)} attention tensors.") + shape = attentions[0].shape # Look at the first layer's shape + else: + print("Structure: Single stacked tensor.") + shape = attentions.shape + + print(f"Attention Shape: {shape}") + + if shape[-1] == N and shape[-2] == N: + print("Token slicing is PERFECT! The and tokens were successfully removed.") + elif shape[-1] == N + 2: + print("Token slicing failed. The padding tokens are still in the tensor.") + else: + print(f"Unexpected attention dimensions. Expected {N}x{N}, got {shape[-2]}x{shape[-1]}") + else: + print("No attention trace found in the dictionary.") + + print("-" * 40) + + # 2. Check Activations (Single Representations) + activations = data.get('activations') + if activations is not None: + if isinstance(activations, list): + act_shape = activations[0].shape + else: + act_shape = activations.shape + + print(f"Activations Shape: {act_shape}") + else: + print("No 'activations' found. (Expected since we are waiting to push your s_s extraction!)") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python test_trace.py ") + else: + validate_trace(sys.argv[1]) \ No newline at end of file diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index 43febde2..6b290310 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -48,6 +48,8 @@ def __init__( self.activations: Dict[str, torch.Tensor] = {} self._handles: List[Any] = [] self._patched_forwards: List[Tuple[nn.Module, Callable]] = [] + self.recycled_s_s: List[torch.Tensor] = [] + self.recycled_s_z: List[torch.Tensor] = [] def clear(self) -> None: self.attention.clear() @@ -171,6 +173,28 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: self.activations[key] = h.detach() return hook + def _make_trunk_hook(self) -> Callable: + """Hook that captures s_s and s_z at every recycling iteration.""" + def hook(module: nn.Module, inp: Any, out: Any) -> None: + s_s, s_z = None, None + + # Handle HF returning a tuple (usually s_s is index 0 and s_z is index 1) + if isinstance(out, tuple) and len(out) >= 2: + s_s, s_z = out[0], out[1] + # Handle HF returning a dataclass or object + elif hasattr(out, 's_s') and hasattr(out, 's_z'): + s_s, s_z = out.s_s, out.s_z + # Handle dictionaries + elif isinstance(out, dict): + s_s, s_z = out.get('s_s'), out.get('s_z') + + if s_s is not None and s_z is not None: + # Squeeze out the batch dimension and move to CPU to prevent RAM crashes + # s_s shape: [N, 1024] | s_z shape: [N, N, 128] + self.recycled_s_s.append(s_s.squeeze(0).cpu().detach()) + self.recycled_s_z.append(s_z.squeeze(0).cpu().detach()) + return hook + class StructureModuleTraceCollector: """ diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index c5c2480e..b7703ba4 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -199,6 +199,10 @@ def log(msg: str) -> None: if trace_mode != "none": esm_trunk = getattr(model, "esm", model) collector.register_hooks(esm_trunk) + # Hook the folding trunk to catch recycling iterations + if hasattr(model, "trunk"): + trunk_handle = model.trunk.register_forward_hook(collector._make_trunk_hook()) + collector._handles.append(trunk_handle) # Structure module hooks: IPA attention + per-recycle backbone sm_collector = None @@ -214,6 +218,14 @@ def log(msg: str) -> None: if trace_mode != "none": collector.remove_hooks() + # Archive the recycled s_s and s_z tensors we caught + if want_act and len(collector.recycled_s_s) > 0: + num_iters = len(collector.recycled_s_s) + log(f"[{self.model_name}] [{trace_mode}] Captured {num_iters} trunk recycling iterations.") + for i in range(num_iters): + collector.activations[f"recycle_{i}_s_s"] = collector.recycled_s_s[i] + collector.activations[f"recycle_{i}_s_z"] = collector.recycled_s_z[i] + # out.s_s: folding trunk single representations [B, N, 1024]. # These are the per-residue embeddings produced by ESMFold's # structure module, complementing the ESM-2 encoder traces. From d86c4adee94adc838451023118e9bf47fab1bc85 Mon Sep 17 00:00:00 2001 From: Jeeva Ramasamy <94267209+JeevanandanRamasamy@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:19:46 -0400 Subject: [PATCH 13/18] Extract Evoformer Trunk Intermediates from ESMFold (#4) * Add VizFold text-file attention export compatible with existing visualization tools * Bug fix: override the positional arg in-place instead of adding to kwargs * Fix: trace_formats missing from meta.json * Robust attention saving & forward signature handling hooks.py: - make the EsmSelfAttention forward patch resilient to signature changes by finding the position of output_attentions by name instead of assuming a fixed positional index trace_adapter.py: - reuse OpenFold's save_attention_topk if available, and falls back to a self-contained NumPy implementation (no OpenFold dependency) that writes msa_row_attn text files - layer-index extraction via regex - compute produced trace_formats dynamically in build_and_write_meta instead of hardcoding ["pt","txt"] * Capture and save evoformer trunk intermediates Add per-block evoformer tracing and output saving for ESMFold. - hooks.py: introduce register_trunk_hooks and _make_trunk_block_hook to register forward hooks on model.trunk.blocks (EsmFoldTriangularSelfAttentionBlock). Captured per-block sequence_state and pairwise_state are stored in collector.trunk_blocks; clear() updated and warnings added when trunk/blocks are missing. - inference.py: register the new trunk hooks in ESMFoldRunner, extract and save final folding trunk pair representations (out.s_z), and write per-block evoformer intermediates to trace/trunk/*.pt while recording shapes. Logging messages adjusted. - trace_adapter.py: update trace layout to include trunk/ files (block_{idx}_seq/pair, s_s, s_z). * ESMFold: save trunk tensors, CPU attention Ensure attention tensors are moved to CPU in hooks (detach().cpu()) to avoid GPU tensor serialization. Stop extracting final trunk outputs from model.out and instead collect final s_s/s_z from collector.recycled_s_s/recycled_s_z (avoids redundant copies) and save per-block trunk tensors plus final s_s/s_z into trace/trunk/. * Squeeze batch dim in hooks; drop recycling archive Fix tensor shape handling in ESMFoldTraceCollector hooks by squeezing the leading batch dimension before detaching and moving seq and pair states to CPU, preventing stored activations from containing an extra batch axis. Also remove the prior archival of recycled s_s/s_z tensors in the ESMFoldRunner inference flow to avoid redundant/memory-heavy activation copies and logging related to those recycled tensors. --- vizfold/backends/esmfold/__init__.py | 2 +- vizfold/backends/esmfold/hooks.py | 77 ++++++++++++++++++++--- vizfold/backends/esmfold/inference.py | 54 ++++++++-------- vizfold/backends/esmfold/trace_adapter.py | 10 ++- 4 files changed, 105 insertions(+), 38 deletions(-) diff --git a/vizfold/backends/esmfold/__init__.py b/vizfold/backends/esmfold/__init__.py index fc97282e..17a2a859 100644 --- a/vizfold/backends/esmfold/__init__.py +++ b/vizfold/backends/esmfold/__init__.py @@ -1,5 +1,5 @@ """ -ESMFold backend: inference and trace export for ESMFold (fair-esm). +ESMFold backend: inference and trace export via HuggingFace Transformers. """ # Lazy import so schema/ can be used without requiring torch/fair-esm def __getattr__(name): diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index 6b290310..70382bf3 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -2,11 +2,23 @@ Hook-based extraction of attention weights and hidden states from HuggingFace ESMFold. HF EsmForProteinFolding does NOT support output_attentions / output_hidden_states. -We capture traces by registering forward hooks on the ESM-2 trunk: +We capture traces by registering forward hooks on three stages: - Attention weights: hook on each EsmSelfAttention module, monkey-patching - the forward to force attn_weights to be returned. - Activations: hook on each EsmLayer (full transformer block output). + 1. ESM-2 trunk (model.esm): + Attention weights: hook on each EsmSelfAttention module, monkey-patching + the forward to force attn_weights to be returned. + Activations: hook on each EsmLayer (full transformer block output). + + 2. Folding trunk (model.trunk): + Per-block: hook on each EsmFoldTriangularSelfAttentionBlock to capture + sequence_state [B, L, C_s] and pairwise_state [B, L, L, C_z]. + Final: capture out.s_s and out.s_z from the model output. + + 3. Structure module (model.trunk.structure_module): + IPA attention: StructureModuleTraceCollector wraps ipa.softmax to capture + the attention matrix [H, N, N] at each block per recycle. + Backbone: hook on structure_module to capture per-recycle positions + and single representations. The ESM-2 tokenizer adds and tokens, so attention maps are (seq_len+2, seq_len+2). We slice out the special tokens so stored tensors @@ -29,8 +41,8 @@ class ESMFoldTraceCollector: """ - Collects attention weights and/or hidden states from the ESM-2 trunk - inside HuggingFace EsmForProteinFolding. + Collects attention weights, hidden states, and folding trunk intermediates + from HuggingFace EsmForProteinFolding. """ def __init__( @@ -44,16 +56,22 @@ def __init__( self.want_activations = want_activations self.layer_indices = layer_indices # None => all self.head_indices = head_indices + # ESM-2 encoder outputs self.attention: Dict[str, torch.Tensor] = {} self.activations: Dict[str, torch.Tensor] = {} self._handles: List[Any] = [] self._patched_forwards: List[Tuple[nn.Module, Callable]] = [] self.recycled_s_s: List[torch.Tensor] = [] self.recycled_s_z: List[torch.Tensor] = [] + # Per-block evoformer intermediates from trunk.blocks[i] + self.trunk_blocks: Dict[str, torch.Tensor] = {} def clear(self) -> None: self.attention.clear() self.activations.clear() + self.recycled_s_s.clear() + self.recycled_s_z.clear() + self.trunk_blocks.clear() def _should_store_layer(self, layer_idx: int) -> bool: return self.layer_indices is None or layer_idx in self.layer_indices @@ -161,7 +179,7 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: if attn_weights.dim() == 4 and attn_weights.shape[-1] >= 3: attn_weights = attn_weights[:, :, 1:-1, 1:-1] key = f"layer_{layer_idx:03d}" - self.attention[key] = attn_weights.detach() + self.attention[key] = attn_weights.detach().cpu() return hook def _make_activation_hook(self, layer_idx: int) -> Callable: @@ -195,6 +213,51 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: self.recycled_s_z.append(s_z.squeeze(0).cpu().detach()) return hook + # ------------------------------------------------------------------- + # Per-block evoformer hooks + # ------------------------------------------------------------------- + + def register_trunk_hooks(self, model: nn.Module) -> None: + """ + Register hooks on each EsmFoldTriangularSelfAttentionBlock inside + model.trunk.blocks to capture per-block sequence_state [B, L, C_s] + and pairwise_state [B, L, L, C_z]. + + Note: these tensors can be large (pair state is L×L×C_z per block). + """ + trunk = getattr(model, "trunk", None) + if trunk is None: + warnings.warn("model.trunk not found; trunk block hooks skipped.", UserWarning) + return + blocks = getattr(trunk, "blocks", None) + if blocks is None or not isinstance(blocks, nn.ModuleList): + warnings.warn("model.trunk.blocks not found; trunk block hooks skipped.", UserWarning) + return + + for block_idx, block in enumerate(blocks): + h = block.register_forward_hook(self._make_trunk_block_hook(block_idx)) + self._handles.append(h) + + def _make_trunk_block_hook(self, block_idx: int) -> Callable: + """ + Hook for EsmFoldTriangularSelfAttentionBlock. + Output is (sequence_state, pairwise_state). + + Note: trunk blocks are called once per recycle iteration. Since dict + keys are the same across iterations, only the LAST recycle's data + is retained. This is intentional — the final iteration is the most + refined representation. + """ + def hook(module: nn.Module, inp: Any, out: Any) -> None: + if not isinstance(out, tuple) or len(out) < 2: + return + seq_state, pair_state = out[0], out[1] + if isinstance(seq_state, torch.Tensor): + self.trunk_blocks[f"block_{block_idx:03d}_seq"] = seq_state.squeeze(0).detach().cpu() + if isinstance(pair_state, torch.Tensor): + self.trunk_blocks[f"block_{block_idx:03d}_pair"] = pair_state.squeeze(0).detach().cpu() + return hook + class StructureModuleTraceCollector: """ diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index b7703ba4..676a6b9f 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -4,7 +4,6 @@ Uses HuggingFace Transformers (EsmForProteinFolding) to avoid OpenFold build dependency. """ import os -import sys import warnings from typing import Any, Dict, List, Optional, Tuple @@ -203,6 +202,8 @@ def log(msg: str) -> None: if hasattr(model, "trunk"): trunk_handle = model.trunk.register_forward_hook(collector._make_trunk_hook()) collector._handles.append(trunk_handle) + # Hook each evoformer block to capture per-block sequence/pair state + collector.register_trunk_hooks(model) # Structure module hooks: IPA attention + per-recycle backbone sm_collector = None @@ -214,27 +215,9 @@ def log(msg: str) -> None: with torch.no_grad(): out = model(input_ids=input_ids, attention_mask=attention_mask) - single_reps = None if trace_mode != "none": collector.remove_hooks() - # Archive the recycled s_s and s_z tensors we caught - if want_act and len(collector.recycled_s_s) > 0: - num_iters = len(collector.recycled_s_s) - log(f"[{self.model_name}] [{trace_mode}] Captured {num_iters} trunk recycling iterations.") - for i in range(num_iters): - collector.activations[f"recycle_{i}_s_s"] = collector.recycled_s_s[i] - collector.activations[f"recycle_{i}_s_z"] = collector.recycled_s_z[i] - - # out.s_s: folding trunk single representations [B, N, 1024]. - # These are the per-residue embeddings produced by ESMFold's - # structure module, complementing the ESM-2 encoder traces. - if hasattr(out, 's_s') and out.s_s is not None: - single_reps = out.s_s.squeeze(0).cpu() - log(f"[{self.model_name}] [{trace_mode}] Extracted folding trunk s_s: {single_reps.shape}") - else: - log(f"[{self.model_name}] [{trace_mode}] out.s_s not found — folding trunk single representations missing.") - if sm_collector is not None: sm_collector.remove_hooks() log(f"Structure module traces: {len(sm_collector.ipa_attention)} IPA blocks, " @@ -262,15 +245,30 @@ def log(msg: str) -> None: shapes_recorded["attention"][k] = v.get("shape", []) for k, v in act_idx.items(): shapes_recorded["activations"][k] = v.get("shape", []) - if single_reps is not None: - s_s_path = os.path.join(out_dir, "trace", "activations", "folding_trunk_s_s.pt") - torch.save(single_reps if not save_fp16 else single_reps.half(), s_s_path) - shapes_recorded["activations"]["folding_trunk_s_s"] = { - "path": os.path.relpath(s_s_path, out_dir), - "dtype": str(single_reps.dtype), - "shape": list(single_reps.shape), - } - log(f"Folding trunk s_s written to {s_s_path}") + # Save per-block evoformer intermediates + final s_s/s_z into trace/trunk/ + trunk_tensors = dict(collector.trunk_blocks) # block_000_seq, block_000_pair, ... + # Add final trunk outputs: recycled_s_s[-1] == out.s_s (last recycle iteration). + # Saved here rather than from out.s_s to avoid a redundant copy. + if collector.recycled_s_s: + trunk_tensors["s_s"] = collector.recycled_s_s[-1] # [L, C_s] + if collector.recycled_s_z: + trunk_tensors["s_z"] = collector.recycled_s_z[-1] # [L, L, C_z] + + if trunk_tensors: + trunk_dir = os.path.join(out_dir, "trace", "trunk") + os.makedirs(trunk_dir, exist_ok=True) + shapes_recorded["trunk"] = {} + for key, t in trunk_tensors.items(): + path = os.path.join(trunk_dir, f"{key}.pt") + torch.save(t if not save_fp16 else t.half(), path) + shapes_recorded["trunk"][key] = { + "path": os.path.relpath(path, out_dir), + "shape": list(t.shape), + } + log(f"Evoformer trunk intermediates written to {trunk_dir} " + f"({len(trunk_tensors)} tensors: " + f"{len(collector.trunk_blocks)} blocks + " + f"{len(trunk_tensors) - len(collector.trunk_blocks)} final)") try: write_trace_summary(out_dir, collector) diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py index fa8c317d..6f428bda 100644 --- a/vizfold/backends/esmfold/trace_adapter.py +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -14,6 +14,12 @@ activations/ layer_000.pt ... + trunk/ + block_000_seq.pt [L, C_s] per-block sequence state (last recycle) + block_000_pair.pt [L, L, C_z] per-block pair state (last recycle) + ... + s_s.pt [L, C_s] final trunk single representations + s_z.pt [L, L, C_z] final trunk pair representations index.json attention_files/ msa_row_attn_layer0.txt (VizFold text format) @@ -207,9 +213,9 @@ def write_trace_summary( summary["attention"][layer_key] = { "mean": float(block.mean()), "std": float(block.std()), - "entropy_proxy": float(ent), + "entropy_proxy": float(ent), "sparsity_proxy": float((block < 1e-5).mean()), - } + } for key, t in collector.activations.items(): h = t.float().cpu().numpy() if h.size == 0: From f3618ce995b88c79103cad6e4851a0bc369e7552 Mon Sep 17 00:00:00 2001 From: Mose-Kim02 <114587148+Mose-Kim02@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:45:37 -0400 Subject: [PATCH 14/18] Add validation tests for ESMFold intermediate outputs (#5) * Add ESMFold backend smoke test and reproducibility documentation * Add tensor shape validation and fix smoke test per review * Fix smoke test per review: tmp_path, sys.executable, tensor shape validation * Fix ESMFold smoke test per review and validate attention tensor shape * Add validation for trunk intermediates, attention exports, and new ESMFold outputs * Address review comments: remove duplicate smoke test file and update recycle output paths --- docs/esmfold_backend_repro.md | 25 +++++ tests/test_esmf_smoke.py | 187 ++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/docs/esmfold_backend_repro.md b/docs/esmfold_backend_repro.md index 00f3a788..5f681ef8 100644 --- a/docs/esmfold_backend_repro.md +++ b/docs/esmfold_backend_repro.md @@ -154,3 +154,28 @@ Expected outputs: - `trace/activations/` GPU execution confirms cluster compatibility for larger inference workloads. + + +## Additional Intermediate Output Validation + +The latest shared `feature/esmfold-backend` branch now exports additional intermediate outputs beyond the original encoder attention and activation traces. + +Verified local outputs include: + +- 36 attention tensors in `trace/attention/` +- 36 activation tensors in `trace/activations/` +- ~98 Evoformer trunk intermediate tensors in `trace/trunk/` +- 36 VizFold attention text files in `attention_files/` + +Expected tensor shapes include: + +- attention tensors: `[B, H, N, N]` +- activation tensors: `[B, N, D]` +- pair representations (`s_z`): `[N, N, D]` + +If recycling outputs are enabled, they are expected to appear under `trace/trunk/` with keys such as: + +- `recycle_*_s_s` +- `recycle_*_s_z` + +If structure-module / IPA outputs are enabled, they should also be saved as `.pt` tensors in the trace archive and can be validated separately for expected attention dimensions. \ No newline at end of file diff --git a/tests/test_esmf_smoke.py b/tests/test_esmf_smoke.py index 23b06756..7cae912b 100644 --- a/tests/test_esmf_smoke.py +++ b/tests/test_esmf_smoke.py @@ -11,6 +11,7 @@ import subprocess import sys import tempfile +import torch from pathlib import Path # Repo root @@ -242,3 +243,189 @@ def test_esmf_trace_extraction(tmp_path=None): print("Failed:", failed) sys.exit(1) print("All checks passed.") + +import os +import subprocess +import sys +import torch + + +def test_esmfold_backend_smoke(tmp_path): + output_dir = tmp_path / "test_trace_ci" + + cmd = [ + sys.executable, + "run_pretrained_esmf.py", + "--fasta", + "examples/monomer/fasta_dir_6KWC/6KWC.fasta", + "--out", + str(output_dir), + "--trace_mode", + "attention+activations", + "--device", + "cpu", + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + # expected top-level outputs + assert os.path.exists(output_dir / "meta.json") + assert os.path.exists(output_dir / "structure" / "predicted.pdb") + assert os.path.exists(output_dir / "trace") + + # collect all .pt trace files + trace_files = [] + for root, _, files in os.walk(output_dir / "trace"): + trace_files += [ + os.path.join(root, f) for f in files if f.endswith(".pt") + ] + + assert len(trace_files) >= 72 + + # validate attention tensor shape specifically + attention_dir = output_dir / "trace" / "attention" + assert attention_dir.exists() + + attention_files = [ + attention_dir / f + for f in os.listdir(attention_dir) + if f.endswith(".pt") + ] + + assert len(attention_files) == 36 + + sample_attention = torch.load(attention_files[0], map_location="cpu") + assert len(sample_attention.shape) == 4 + assert sample_attention.shape[0] == 1 + assert sample_attention.shape[2] == sample_attention.shape[3] + + # validate activation tensors exist + activation_dir = output_dir / "trace" / "activations" + assert activation_dir.exists() + + activation_files = [ + activation_dir / f + for f in os.listdir(activation_dir) + if f.endswith(".pt") + ] + + assert len(activation_files) == 36 + + sample_activation = torch.load(activation_files[0], map_location="cpu") + assert len(sample_activation.shape) == 3 + assert sample_activation.shape[0] == 1 + + # validate Evoformer trunk intermediates + trunk_dir = output_dir / "trace" / "trunk" + assert trunk_dir.exists() + + trunk_files = [ + trunk_dir / f + for f in os.listdir(trunk_dir) + if f.endswith(".pt") + ] + + assert len(trunk_files) >= 96 + + # if final or pair outputs exist, validate s_z style shape [N, N, D] + trunk_sz_candidates = [ + f for f in trunk_files + if "s_z" in f.name or "pair" in f.name + ] + if trunk_sz_candidates: + sample_sz = torch.load(trunk_sz_candidates[0], map_location="cpu") + assert len(sample_sz.shape) == 3 + assert sample_sz.shape[0] == sample_sz.shape[1] + + # validate attention text export + attention_txt_dir = output_dir / "attention_files" + assert attention_txt_dir.exists() + + attention_txt_files = [ + attention_txt_dir / f + for f in os.listdir(attention_txt_dir) + if f.endswith(".txt") + ] + + assert len(attention_txt_files) == 36 + + # optional validation for recycling outputs if present + recycle_ss = [ + f for f in activation_files + if "recycle_" in f.name and "_s_s" in f.name + ] + recycle_sz = [ + f for f in activation_files + if "recycle_" in f.name and "_s_z" in f.name + ] + + if recycle_ss: + sample_recycle_ss = torch.load(recycle_ss[0], map_location="cpu") + assert len(sample_recycle_ss.shape) == 3 + + if recycle_sz: + sample_recycle_sz = torch.load(recycle_sz[0], map_location="cpu") + assert len(sample_recycle_sz.shape) == 3 + assert sample_recycle_sz.shape[0] == sample_recycle_sz.shape[1] + + # optional validation for structure-module / IPA outputs if present + ipa_candidates = [] + for root, _, files in os.walk(output_dir / "trace"): + ipa_candidates += [ + os.path.join(root, f) + for f in files + if f.endswith(".pt") and "ipa" in f.lower() + ] + + if ipa_candidates: + sample_ipa = torch.load(ipa_candidates[0], map_location="cpu") + assert len(sample_ipa.shape) >= 3 + +def test_esmfold_full_validation(tmp_path): + output_dir = tmp_path / "test_trace_ci" + + cmd = [ + sys.executable, + "run_pretrained_esmf.py", + "--fasta", + "examples/monomer/fasta_dir_6KWC/6KWC.fasta", + "--out", + str(output_dir), + "--trace_mode", + "attention+activations", + "--device", + "cpu", + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + # expected outputs + assert os.path.exists(f"{output_dir}/meta.json") + assert os.path.exists(f"{output_dir}/structure/predicted.pdb") + assert os.path.exists(f"{output_dir}/trace") + + # collect all trace tensor files + trace_files = [] + for root, _, files in os.walk(f"{output_dir}/trace"): + trace_files += [ + os.path.join(root, f) for f in files if f.endswith(".pt") + ] + + assert len(trace_files) >= 36 + + # validate attention tensor shape specifically + attention_dir = os.path.join(output_dir, "trace", "attention") + attention_files = [ + os.path.join(attention_dir, f) + for f in os.listdir(attention_dir) + if f.endswith(".pt") + ] + + assert len(attention_files) >= 1 + + sample_tensor = torch.load(attention_files[0], map_location="cpu") + assert len(sample_tensor.shape) == 4 + assert sample_tensor.shape[0] == 1 + assert sample_tensor.shape[2] == sample_tensor.shape[3] \ No newline at end of file From bc3505b9998ba0e562b1613c4f461bf16e2ddd06 Mon Sep 17 00:00:00 2001 From: Jeeva Ramasamy <94267209+JeevanandanRamasamy@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:59:13 -0400 Subject: [PATCH 15/18] ESMFold backend correctness and cleanup (#7) * Add VizFold text-file attention export compatible with existing visualization tools * Bug fix: override the positional arg in-place instead of adding to kwargs * Fix: trace_formats missing from meta.json * Robust attention saving & forward signature handling hooks.py: - make the EsmSelfAttention forward patch resilient to signature changes by finding the position of output_attentions by name instead of assuming a fixed positional index trace_adapter.py: - reuse OpenFold's save_attention_topk if available, and falls back to a self-contained NumPy implementation (no OpenFold dependency) that writes msa_row_attn text files - layer-index extraction via regex - compute produced trace_formats dynamically in build_and_write_meta instead of hardcoding ["pt","txt"] * Capture and save evoformer trunk intermediates Add per-block evoformer tracing and output saving for ESMFold. - hooks.py: introduce register_trunk_hooks and _make_trunk_block_hook to register forward hooks on model.trunk.blocks (EsmFoldTriangularSelfAttentionBlock). Captured per-block sequence_state and pairwise_state are stored in collector.trunk_blocks; clear() updated and warnings added when trunk/blocks are missing. - inference.py: register the new trunk hooks in ESMFoldRunner, extract and save final folding trunk pair representations (out.s_z), and write per-block evoformer intermediates to trace/trunk/*.pt while recording shapes. Logging messages adjusted. - trace_adapter.py: update trace layout to include trunk/ files (block_{idx}_seq/pair, s_s, s_z). * ESMFold: save trunk tensors, CPU attention Ensure attention tensors are moved to CPU in hooks (detach().cpu()) to avoid GPU tensor serialization. Stop extracting final trunk outputs from model.out and instead collect final s_s/s_z from collector.recycled_s_s/recycled_s_z (avoids redundant copies) and save per-block trunk tensors plus final s_s/s_z into trace/trunk/. * Squeeze batch dim in hooks; drop recycling archive Fix tensor shape handling in ESMFoldTraceCollector hooks by squeezing the leading batch dimension before detaching and moving seq and pair states to CPU, preventing stored activations from containing an extra batch axis. Also remove the prior archival of recycled s_s/s_z tensors in the ESMFoldRunner inference flow to avoid redundant/memory-heavy activation copies and logging related to those recycled tensors. * Enhance ESMFold tracing, structure & sanity checks Add structure-module tracing output and update docs to pin transformers>=4.36.0 and show example for IPA attention + per-recycle backbone traces. Improve HPC slurm script to fail fast if torch/transformers aren't importable. Strengthen trace hooks: validate attention slice length against an expected_seq_len, perform head filtering in-hook, detach tensors to CPU, and add a helper to extract positions/frames/single from dict or dataclass outputs so structure module traces are robust. Small fixes: reset internal counters after each recycle, remove redundant head-filtering in trace_adapter, and tidy FASTA reading variable naming. * Implement BackendBase in ESMFoldRunner Make ESMFoldRunner inherit from BackendBase and implement the backend interface (load_model, run_inference, supports_attention, supports_activations). Refactor internal loading into _load_model and update usage sites to call it; add trace configuration handling and pass expected_seq_len to the trace collector. Also remove redundant imports from tests/test_esmf_smoke.py. * Bug Fix: Strip / from transformer activations Update activation hook to remove the leading and trailing tokens so the activation tensor [B, N, D] aligns with attention maps [B, H, N, N]. --- docs/esmfold.md | 36 +++++++++- scripts/hpc/ice/run_esmf_ice.slurm | 10 +++ tests/test_esmf_smoke.py | 6 +- vizfold/backends/esmfold/hooks.py | 84 +++++++++++++++++++---- vizfold/backends/esmfold/inference.py | 60 +++++++++++++++- vizfold/backends/esmfold/schema.py | 2 +- vizfold/backends/esmfold/trace_adapter.py | 6 +- 7 files changed, 174 insertions(+), 30 deletions(-) diff --git a/docs/esmfold.md b/docs/esmfold.md index afd75725..204cc0ff 100644 --- a/docs/esmfold.md +++ b/docs/esmfold.md @@ -20,7 +20,7 @@ pip install -r requirements-esmfold.txt # Optional: pip install -e . for vizfold package ``` -`requirements-esmfold.txt` includes: `transformers` (and optionally `torch` if not already installed). +`requirements-esmfold.txt` pins `transformers>=4.36.0`. PyTorch must be installed separately. ## Run locally @@ -57,6 +57,17 @@ python run_pretrained_esmf.py \ --heads 0,1,2 ``` +**Structure + IPA attention + per-recycle backbone (structure module traces):** + +```bash +python run_pretrained_esmf.py \ + --fasta examples/monomer/fasta_dir_6KWC/6KWC.fasta \ + --out outputs/esmf_6KWC \ + --trace_mode attention+activations \ + --structure_traces \ + --save_fp16 +``` + ## Output layout After a run, `--out` contains: @@ -75,7 +86,25 @@ outputs/esmf_6KWC/ activations/ layer_000.pt ... - index.json # Maps layer/head to path, dtype, shape + trunk/ # Evoformer intermediates (per-block + final) + block_000_seq.pt # [L, C_s] per-block sequence state (last recycle) + block_000_pair.pt # [L, L, C_z] per-block pair state (last recycle) + ... + s_s.pt # [L, C_s] final trunk single representations + s_z.pt # [L, L, C_z] final trunk pair representations + structure_module/ # Only with --structure_traces + ipa_attention/ + recycle_00_block_00.pt # IPA attention [H, N, N] + ... + backbone/ + recycle_00_positions.pt # Per-recycle backbone coords + recycle_00_states.pt # Per-recycle single representations + ... + summary.json # Per-layer attention entropy, sparsity, norms + index.json # Maps layer/head to path, dtype, shape + attention_files/ + msa_row_attn_layer0.txt # VizFold text format (top-k per head) + ... logs.txt # Log lines from the run ``` @@ -86,7 +115,8 @@ Includes: - `backend`, `model_name`, `date_time`, `device`, `dtype` - `sequence_length`, `input_fasta_hash`, `input_fasta_path` - `layer_count`, `head_count`, `trace_mode`, `tensor_format` (fp16/fp32) -- `shapes_recorded`: per-file shapes for attention and activations +- `trace_formats`: which output formats were produced (`pt`, `txt`) +- `shapes_recorded`: per-file shapes for attention, activations, trunk, and structure module - `seed`, `deterministic` (if set) - `repo_commit` (if run from a git repo) diff --git a/scripts/hpc/ice/run_esmf_ice.slurm b/scripts/hpc/ice/run_esmf_ice.slurm index acbe4652..892c4de6 100644 --- a/scripts/hpc/ice/run_esmf_ice.slurm +++ b/scripts/hpc/ice/run_esmf_ice.slurm @@ -37,6 +37,16 @@ mkdir -p "$OUTDIR" # conda activate vizfold cd "${VIZFOLD_REPO:-.}" + +# Sanity check: fail fast if the environment is not set up +python -c "import torch; import transformers" 2>/dev/null || { + echo "ERROR: torch or transformers not importable." + echo "Activate your conda env before submitting, e.g.:" + echo " conda activate vizfold" + echo "Or uncomment the module/conda lines above in this script." + exit 1 +} + echo "FASTA=$FASTA OUTDIR=$OUTDIR TRACE_MODE=$TRACE_MODE" echo "Running ESMFold..." diff --git a/tests/test_esmf_smoke.py b/tests/test_esmf_smoke.py index 7cae912b..6319a08b 100644 --- a/tests/test_esmf_smoke.py +++ b/tests/test_esmf_smoke.py @@ -244,11 +244,6 @@ def test_esmf_trace_extraction(tmp_path=None): sys.exit(1) print("All checks passed.") -import os -import subprocess -import sys -import torch - def test_esmfold_backend_smoke(tmp_path): output_dir = tmp_path / "test_trace_ci" @@ -382,6 +377,7 @@ def test_esmfold_backend_smoke(tmp_path): sample_ipa = torch.load(ipa_candidates[0], map_location="cpu") assert len(sample_ipa.shape) >= 3 + def test_esmfold_full_validation(tmp_path): output_dir = tmp_path / "test_trace_ci" diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index 70382bf3..1e05fd7f 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -30,12 +30,12 @@ recycling iteration, and hooks on trunk.structure_module to capture per-recycle backbone positions and single representations. """ +import inspect import re import warnings from typing import Any, Callable, Dict, List, Optional, Tuple import torch -import inspect import torch.nn as nn @@ -51,11 +51,13 @@ def __init__( want_activations: bool = True, layer_indices: Optional[List[int]] = None, head_indices: Optional[List[int]] = None, + expected_seq_len: Optional[int] = None, ): self.want_attention = want_attention self.want_activations = want_activations self.layer_indices = layer_indices # None => all self.head_indices = head_indices + self.expected_seq_len = expected_seq_len # ESM-2 encoder outputs self.attention: Dict[str, torch.Tensor] = {} self.activations: Dict[str, torch.Tensor] = {} @@ -65,6 +67,7 @@ def __init__( self.recycled_s_z: List[torch.Tensor] = [] # Per-block evoformer intermediates from trunk.blocks[i] self.trunk_blocks: Dict[str, torch.Tensor] = {} + self._slice_validated = False def clear(self) -> None: self.attention.clear() @@ -72,6 +75,7 @@ def clear(self) -> None: self.recycled_s_s.clear() self.recycled_s_z.clear() self.trunk_blocks.clear() + self._slice_validated = False def _should_store_layer(self, layer_idx: int) -> bool: return self.layer_indices is None or layer_idx in self.layer_indices @@ -178,17 +182,47 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: # Slice out special tokens -> [B, H, N, N] if attn_weights.dim() == 4 and attn_weights.shape[-1] >= 3: attn_weights = attn_weights[:, :, 1:-1, 1:-1] + + # Validate that the sliced shape matches expected sequence length + if (self.expected_seq_len is not None + and not self._slice_validated + and attn_weights.dim() == 4): + actual_n = attn_weights.shape[-1] + if actual_n != self.expected_seq_len: + warnings.warn( + f"Attention slice mismatch: after removing special tokens, " + f"attention dimension is {actual_n} but expected sequence " + f"length is {self.expected_seq_len}. Attention maps may be " + f"misaligned with residue indices.", + UserWarning, + ) + self._slice_validated = True + + # Filter heads if requested, to save memory + if self.head_indices is not None: + head_dim = 1 if attn_weights.dim() == 4 else 0 + idx = torch.tensor(self.head_indices, device=attn_weights.device) + attn_weights = attn_weights.index_select(head_dim, idx) + key = f"layer_{layer_idx:03d}" self.attention[key] = attn_weights.detach().cpu() return hook def _make_activation_hook(self, layer_idx: int) -> Callable: - """Hook that captures the transformer layer output (hidden state).""" + """Hook that captures the transformer layer output (hidden state). + + Strips the leading and trailing special tokens so the + activation shape [B, N, D] matches the attention shape [B, H, N, N]. + """ def hook(module: nn.Module, inp: Any, out: Any) -> None: h = out[0] if isinstance(out, tuple) else out if h is not None and isinstance(h, torch.Tensor) and h.dim() >= 2: + # Strip (index 0) and (index -1) to align with + # attention maps which are already sliced to seq_len. + if h.dim() == 3 and h.shape[1] >= 3: + h = h[:, 1:-1, :] key = f"layer_{layer_idx:03d}" - self.activations[key] = h.detach() + self.activations[key] = h.detach().cpu() return hook def _make_trunk_hook(self) -> Callable: @@ -245,8 +279,9 @@ def _make_trunk_block_hook(self, block_idx: int) -> Callable: Note: trunk blocks are called once per recycle iteration. Since dict keys are the same across iterations, only the LAST recycle's data - is retained. This is intentional — the final iteration is the most - refined representation. + is retained. This matches the behavior of recycled_s_s/s_z where + only the final values are used downstream. All recycle iterations + are captured in recycled_s_s/recycled_s_z lists for analysis if needed. """ def hook(module: nn.Module, inp: Any, out: Any) -> None: if not isinstance(out, tuple) or len(out) < 2: @@ -269,9 +304,10 @@ class StructureModuleTraceCollector: attention matrix a [*, H, N, N] before it's consumed internally. Fires num_blocks times per recycle (IPA is a single shared module reused across all structure module blocks). - - trunk.structure_module: captures the full output dict per recycle, + - trunk.structure_module: captures the full output per recycle, including stacked positions [num_blocks, B, N, 14, 3], frames, - and the final single representation. + and the final single representation. Handles both dict and + dataclass outputs from HuggingFace. """ def __init__(self) -> None: @@ -358,20 +394,40 @@ def patched_forward(*args, **kwargs): self._patched_forwards.append((ipa, orig_forward)) self._orig_softmax = (ipa, orig_softmax_module) + def _extract_from_output(self, out: Any) -> dict: + """Extract fields from structure module output (dict or dataclass).""" + result = {} + # Try dict access first + if isinstance(out, dict): + for key in ("positions", "frames", "single"): + if key in out: + result[key] = out[key] + else: + # Handle dataclass / namedtuple / object with attributes + for key in ("positions", "frames", "single"): + val = getattr(out, key, None) + if val is not None: + result[key] = val + return result + def _sm_output_hook(self, module: nn.Module, inp: Any, out: Any) -> None: """ Fires once per recycling iteration after the full structure module forward (all blocks). Captures backbone positions and states. + Handles both dict and dataclass outputs from HuggingFace. """ key = f"recycle_{self._recycle_idx:02d}" - if isinstance(out, dict): - if "positions" in out: - self.backbone_positions[key] = out["positions"].detach().cpu() - if "frames" in out: - self.backbone_frames[key] = out["frames"].detach().cpu() - if "single" in out: - self.sm_states[key] = out["single"].detach().cpu() + fields = self._extract_from_output(out) + + if "positions" in fields: + self.backbone_positions[key] = fields["positions"].detach().cpu() + if "frames" in fields: + self.backbone_frames[key] = fields["frames"].detach().cpu() + if "single" in fields: + self.sm_states[key] = fields["single"].detach().cpu() + # Always reset block counter and advance recycle counter, + # regardless of whether we captured any data. self._recycle_idx += 1 self._block_idx = 0 diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 676a6b9f..a1d32462 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -9,6 +9,7 @@ import torch +from vizfold.backends.base import BackendBase from vizfold.backends.esmfold.hooks import ESMFoldTraceCollector, StructureModuleTraceCollector from vizfold.backends.esmfold.schema import _read_fasta_and_hash from vizfold.backends.esmfold.trace_adapter import ( @@ -82,7 +83,7 @@ def read_fasta(fasta_path: str) -> Tuple[str, str, str]: return seq, seq_id, fasta_hash -class ESMFoldRunner: +class ESMFoldRunner(BackendBase): """ Runs ESMFold inference and writes VizFold-compatible output. @@ -109,7 +110,56 @@ def __init__( self._model = None self._tokenizer = None - def load_model(self) -> Any: + # --- BackendBase interface --- + + def load_model( + self, + model_name: Optional[str] = None, + device: Optional[str] = None, + dtype: Optional[str] = None, + **kwargs: Any, + ) -> Any: + """Load the model. Returns the model object.""" + if model_name is not None: + self.model_name = model_name + if device is not None: + self.device = device + if dtype is not None: + self.dtype = dtype + return self._load_model() + + def run_inference( + self, + fasta_path: str, + out_dir: str, + trace_cfg: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Dict[str, Any]: + """Run inference and optionally write traces (BackendBase interface).""" + trace_mode = "attention+activations" + top_k = 50 + if trace_cfg: + trace_mode = trace_cfg.get("trace_mode", trace_mode) + top_k = trace_cfg.get("top_k", top_k) + return self.run( + fasta_path=fasta_path, + out_dir=out_dir, + trace_mode=trace_mode, + top_k=top_k, + **kwargs, + ) + + def supports_attention(self) -> bool: + """Whether this backend can extract attention maps.""" + return True + + def supports_activations(self) -> bool: + """Whether this backend can extract layer activations (hidden states).""" + return True + + # --- Internal model loading --- + + def _load_model(self) -> Any: if self._model is not None: return self._model if self.seed is not None: @@ -168,7 +218,7 @@ def log(msg: str) -> None: "Attention storage is N^2; consider --layers 0,1 or --trace_mode activations." ) - model = self.load_model() + model = self._load_model() tokenizer = self._tokenizer want_attn = "attention" in trace_mode want_act = "activations" in trace_mode @@ -180,6 +230,7 @@ def log(msg: str) -> None: want_activations=want_act, layer_indices=layer_list, head_indices=head_list, + expected_seq_len=seq_len, ) # Tokenize (HF ESMFold: add_special_tokens=False per standard usage) @@ -440,6 +491,9 @@ def _coords_to_minimal_pdb(coords: torch.Tensor, seq: str) -> str: for i in range(min(ca.shape[0], len(seq))): a = ca[i].float().cpu().numpy() res3 = AA_1TO3.get(seq[i], "UNK") + # PDB ATOM format: columns 1-6 record type, 7-11 serial, 13-16 name, + # 17 altLoc, 18-20 resName, 22 chainID, 23-26 resSeq, 27 iCode, + # 31-38 x, 39-46 y, 47-54 z, 55-60 occupancy, 61-66 tempFactor lines.append( f"ATOM {i+1:5d} CA {res3:>3s} A{i+1:4d} " f"{a[0]:8.3f}{a[1]:8.3f}{a[2]:8.3f} 1.00 0.00 C" diff --git a/vizfold/backends/esmfold/schema.py b/vizfold/backends/esmfold/schema.py index 626bcffc..2e476c1f 100644 --- a/vizfold/backends/esmfold/schema.py +++ b/vizfold/backends/esmfold/schema.py @@ -25,7 +25,7 @@ def _git_head(repo_path: Optional[str] = None) -> Optional[str]: def _read_fasta_and_hash(path: str) -> Tuple[str, str]: with open(path) as f: raw = f.read() - lines = [l.strip() for l in raw.splitlines() if l.strip() and not l.startswith(">")] + lines = [line.strip() for line in raw.splitlines() if line.strip() and not line.startswith(">")] seq = "".join(lines) h = hashlib.sha256(raw.encode()).hexdigest()[:16] return seq, h diff --git a/vizfold/backends/esmfold/trace_adapter.py b/vizfold/backends/esmfold/trace_adapter.py index 6f428bda..84585541 100644 --- a/vizfold/backends/esmfold/trace_adapter.py +++ b/vizfold/backends/esmfold/trace_adapter.py @@ -85,10 +85,8 @@ def write_traces( for key, t in collector.attention.items(): path = os.path.join(attn_dir, f"{key}.pt") - if head_indices is not None and t.dim() >= 3: - # Attention shape: [B, H, N, N] (4D) or [H, N, N] (3D) - head_dim = 1 if t.dim() == 4 else 0 - t = t.index_select(head_dim, torch.tensor(head_indices, device=t.device)) + # Head filtering is done in the hook (ESMFoldTraceCollector), so + # tensors here are already filtered if head_indices was specified. _save_tensor(path, t, save_fp16=save_fp16) attention_index[key] = { "path": os.path.relpath(path, out_dir), From 4342a19d2f1ee2addedbc454b9132fab5b34916e Mon Sep 17 00:00:00 2001 From: rohan5986 <136632833+rohan5986@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:57:24 -0400 Subject: [PATCH 16/18] VizFold Interactive Dashboard - 3Dmol Structure & Trace Explorer (#6) * Extract s_s folding trunk activations and enforce safetensors * Update backend pipeline * Capture s_s and s_z at every recycling iteration via trunk hook * Remove test output artifacts * Complete interactive vizfold dashboard with 3Dmol and attention heatmaps * add frontend package dependencies * fix: address final security reviews and add frontend env configuration * add fastapi and uvicorn to backend requirements --------- Co-authored-by: Rohan Singhal --- .gitignore | 10 + frontend/.env | 1 + frontend/.gitignore | 24 + frontend/README.md | 52 + frontend/eslint.config.js | 29 + frontend/index.html | 14 + frontend/package-lock.json | 5406 +++++++++++++++++++++ frontend/package.json | 30 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.css | 65 + frontend/src/App.jsx | 177 + frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/index.css | 111 + frontend/src/main.jsx | 9 + frontend/vite.config.js | 7 + server.py | 102 + vizfold/backends/esmfold/hooks.py | 22 + vizfold/backends/esmfold/inference.py | 25 + vizfold/backends/esmfold/requirements.txt | 2 + 21 files changed, 6113 insertions(+) create mode 100644 frontend/.env create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js create mode 100644 server.py create mode 100644 vizfold/backends/esmfold/requirements.txt diff --git a/.gitignore b/.gitignore index a36e08e2..b84507d3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,13 @@ cutlass/ *.sto *.a3m *.hhr + +# Backend Generated Files +test_output/ +__pycache__/ +*.pt +*.pdb + +# Frontend Dependencies +frontend/node_modules/ +frontend/dist/ diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 00000000..5934e2e7 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8000 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..79465ccf --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,52 @@ +# VizFold Interactive Dashboard + +This is the frontend interface for the VizFold protein visualization pipeline. It is built with React and Vite, designed to connect to the PyTorch/ESMFold backend to visualize 3D structures and internal neural network attention mechanisms in real-time. + +## Features +* **Interactive 3D Viewer:** WebGL-accelerated protein rendering using 3Dmol.js, featuring confidence-based (pLDDT) coloring and clickable residue targeting. +* **Trace Explorer:** Dynamic heatmaps using Plotly.js to visualize ESM-2 Attention layers and Trunk Evolution (s_z) recycling iterations. +* **Synchronized Targeting:** Clicking physical amino acids on the 3D model draws spatial contact crosshairs directly onto the attention heatmaps. + +## How to Run and Test + +### 1. Start the Backend +The frontend requires the FastAPI bridge and test data to function. + +1. Navigate to the root of the repository in your terminal. +2. Activate your Python environment (e.g., `openfold_env`). +3. Generate the required test tensors and PDB files: + ```bash + python3 run_test.py + ``` +4. Start the FastAPI server: + ```bash + # Default (uses test_output/ directory) + python3 server.py + + # Custom directory + python3 server.py --dir your_custom_directory_path + ``` + +### 2. Start the Frontend +Open a separate terminal window and navigate into the `frontend` directory. + +1. Install Node dependencies: + ```bash + npm install + ``` +2. Configure the environment variables: + Create a `.env` file in the `frontend` directory and add the backend API URL: + ```env + VITE_API_URL=http://localhost:8000 + ``` +3. Start the development server: + ```bash + npm run dev + ``` + +### 3. Verification Steps +Open your browser to the local URL provided by Vite. + +1. **Structure Viewer:** Ensure the 3D nanobody model loads correctly and the default "Color: Confidence" setting displays a blue core. +2. **Timeline & Traces:** Switch the Trace Explorer to "ESM-2 Attention" and scrub the slider. Verify the heatmap matrix updates without throwing 404 errors in the console. +3. **Crosshair Targeting:** Click any amino acid on the 3D model. Verify that the residue label pops up and a targeting crosshair instantly appears on the Plotly heatmap. \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..4fa125da --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..587dd269 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + ESMFold Trace Viewer + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..76b7d542 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5406 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.15.0", + "plotly.js": "^3.5.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-plotly.js": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@choojs/findup": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", + "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", + "license": "MIT", + "dependencies": { + "commander": "^2.15.1" + }, + "bin": { + "findup": "bin/findup.js" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", + "license": "ISC" + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "license": "BSD-3-Clause", + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", + "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@plotly/d3": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.2.tgz", + "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", + "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "node_modules/@plotly/d3-sankey-circular": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", + "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", + "license": "MIT", + "dependencies": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0", + "elementary-circuits-directed-graph": "^1.0.4" + } + }, + "node_modules/@plotly/mapbox-gl": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", + "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/@plotly/point-cluster": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", + "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "binary-search-bounds": "^2.0.4", + "clamp": "^1.0.1", + "defined": "^1.0.0", + "dtype": "^2.0.0", + "flatten-vertex-data": "^1.0.2", + "is-obj": "^1.0.1", + "math-log2": "^1.0.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/@plotly/regl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@plotly/regl/-/regl-2.1.2.tgz", + "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@turf/area": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.4.tgz", + "integrity": "sha512-UEQQFw2XwHpozSBAMEtZI3jDsAad4NnHL/poF7/S6zeDCjEBCkt3MYd6DSGH/cvgcOozxH/ky3/rIVSMZdx4vA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.3.4.tgz", + "integrity": "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.3.4.tgz", + "integrity": "sha512-6c3kyTSKBrmiPMe75UkHw6MgedroZ6eR5usEvdlDhXgA3MudFPXIZkMFmMd1h9XeJ9xFfkmq+HPCdF0cOzvztA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz", + "integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.4.tgz", + "integrity": "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-bounds": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-bounds/-/array-bounds-1.0.1.tgz", + "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", + "license": "MIT" + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-normalize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array-normalize/-/array-normalize-1.1.4.tgz", + "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.0" + } + }, + "node_modules/array-range": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-range/-/array-range-1.0.1.tgz", + "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", + "license": "MIT" + }, + "node_modules/bit-twiddle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz", + "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", + "license": "MIT" + }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", + "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", + "license": "MIT" + }, + "node_modules/bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-fit": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/canvas-fit/-/canvas-fit-1.5.0.tgz", + "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", + "license": "MIT", + "dependencies": { + "element-size": "^1.1.1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clamp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", + "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", + "license": "MIT" + }, + "node_modules/color-alpha": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz", + "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", + "license": "MIT", + "dependencies": { + "color-parse": "^1.3.8" + } + }, + "node_modules/color-alpha/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/color-id/-/color-id-1.1.0.tgz", + "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-normalize": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz", + "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1", + "color-rgba": "^2.1.1", + "dtype": "^2.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-rgba": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-2.4.0.tgz", + "integrity": "sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==", + "license": "MIT", + "dependencies": { + "color-parse": "^1.4.2", + "color-space": "^2.0.0" + } + }, + "node_modules/color-parse": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.0.tgz", + "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-rgba": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-3.0.0.tgz", + "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", + "license": "MIT", + "dependencies": { + "color-parse": "^2.0.0", + "color-space": "^2.0.0" + } + }, + "node_modules/color-space": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/color-space/-/color-space-2.3.2.tgz", + "integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==", + "license": "Unlicense" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/country-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz", + "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-font": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-font/-/css-font-1.2.0.tgz", + "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", + "license": "MIT", + "dependencies": { + "css-font-size-keywords": "^1.0.0", + "css-font-stretch-keywords": "^1.0.1", + "css-font-style-keywords": "^1.0.1", + "css-font-weight-keywords": "^1.0.0", + "css-global-keywords": "^1.0.1", + "css-system-font-keywords": "^1.0.0", + "pick-by-alias": "^1.2.0", + "string-split-by": "^1.0.0", + "unquote": "^1.1.0" + } + }, + "node_modules/css-font-size-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", + "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", + "license": "MIT" + }, + "node_modules/css-font-stretch-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", + "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", + "license": "MIT" + }, + "node_modules/css-font-style-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", + "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", + "license": "MIT" + }, + "node_modules/css-font-weight-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", + "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", + "license": "MIT" + }, + "node_modules/css-global-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", + "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", + "license": "MIT" + }, + "node_modules/css-system-font-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", + "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", + "license": "MIT" + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-kerning": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-kerning/-/detect-kerning-2.1.2.tgz", + "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/draw-svg-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/draw-svg-path/-/draw-svg-path-1.0.0.tgz", + "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "~0.1.1", + "normalize-svg-path": "~0.1.0" + } + }, + "node_modules/dtype": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz", + "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz", + "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", + "license": "MIT" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.340", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", + "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/element-size/-/element-size-1.1.1.tgz", + "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", + "license": "MIT" + }, + "node_modules/elementary-circuits-directed-graph": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", + "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", + "license": "MIT", + "dependencies": { + "strongly-connected-components": "^1.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.0.tgz", + "integrity": "sha512-LDicyhrRFrIaheDYryeM2W8gWyZXnAs4zIr2WVPiOSeTmIu2RjR4x/9N0xLaRWZ+9hssBDGo3AadcohuzAvSvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/falafel": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.5.tgz", + "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/falafel/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-isnumeric": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", + "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", + "license": "MIT", + "dependencies": { + "is-string-blank": "^1.0.1" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/flatten-vertex-data": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", + "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", + "license": "MIT", + "dependencies": { + "dtype": "^2.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/font-atlas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/font-atlas/-/font-atlas-2.1.0.tgz", + "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", + "license": "MIT", + "dependencies": { + "css-font": "^1.0.0" + } + }, + "node_modules/font-measure": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/font-measure/-/font-measure-1.2.2.tgz", + "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", + "license": "MIT", + "dependencies": { + "css-font": "^1.2.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", + "license": "ISC" + }, + "node_modules/get-canvas-context": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz", + "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", + "license": "MIT" + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gl-mat4": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz", + "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", + "license": "Zlib" + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/gl-text": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.4.0.tgz", + "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.2", + "color-normalize": "^1.5.0", + "css-font": "^1.2.0", + "detect-kerning": "^2.1.2", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "font-atlas": "^2.1.0", + "font-measure": "^1.2.2", + "gl-util": "^3.1.2", + "is-plain-obj": "^1.1.0", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "parse-unit": "^1.0.1", + "pick-by-alias": "^1.2.0", + "regl": "^2.0.0", + "to-px": "^1.0.1", + "typedarray-pool": "^1.1.0" + } + }, + "node_modules/gl-util": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/gl-util/-/gl-util-3.1.3.tgz", + "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1", + "is-firefox": "^1.0.3", + "is-plain-obj": "^1.1.0", + "number-is-integer": "^1.0.1", + "object-assign": "^4.1.0", + "pick-by-alias": "^1.2.0", + "weak-map": "^1.0.5" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glsl-inject-defines": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", + "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", + "license": "MIT", + "dependencies": { + "glsl-token-inject-block": "^1.0.0", + "glsl-token-string": "^1.0.1", + "glsl-tokenizer": "^2.0.2" + } + }, + "node_modules/glsl-resolve": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz", + "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", + "license": "MIT", + "dependencies": { + "resolve": "^0.6.1", + "xtend": "^2.1.2" + } + }, + "node_modules/glsl-resolve/node_modules/resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", + "license": "MIT" + }, + "node_modules/glsl-resolve/node_modules/xtend": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", + "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/glsl-token-assignments": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", + "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", + "license": "MIT" + }, + "node_modules/glsl-token-defines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", + "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", + "license": "MIT", + "dependencies": { + "glsl-tokenizer": "^2.0.0" + } + }, + "node_modules/glsl-token-depth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", + "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", + "license": "MIT" + }, + "node_modules/glsl-token-descope": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", + "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", + "license": "MIT", + "dependencies": { + "glsl-token-assignments": "^2.0.0", + "glsl-token-depth": "^1.1.0", + "glsl-token-properties": "^1.0.0", + "glsl-token-scope": "^1.1.0" + } + }, + "node_modules/glsl-token-inject-block": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", + "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", + "license": "MIT" + }, + "node_modules/glsl-token-properties": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", + "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", + "license": "MIT" + }, + "node_modules/glsl-token-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", + "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", + "license": "MIT" + }, + "node_modules/glsl-token-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz", + "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", + "license": "MIT" + }, + "node_modules/glsl-token-whitespace-trim": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", + "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", + "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", + "license": "MIT", + "dependencies": { + "through2": "^0.6.3" + } + }, + "node_modules/glsl-tokenizer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/glsl-tokenizer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/glslify": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", + "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", + "license": "MIT", + "dependencies": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + }, + "bin": { + "glslify": "bin.js" + } + }, + "node_modules/glslify-bundle": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-5.1.1.tgz", + "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", + "license": "MIT", + "dependencies": { + "glsl-inject-defines": "^1.0.1", + "glsl-token-defines": "^1.0.0", + "glsl-token-depth": "^1.1.1", + "glsl-token-descope": "^1.0.2", + "glsl-token-scope": "^1.1.1", + "glsl-token-string": "^1.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glsl-tokenizer": "^2.0.2", + "murmurhash-js": "^1.0.0", + "shallow-copy": "0.0.1" + } + }, + "node_modules/glslify-deps": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", + "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", + "license": "ISC", + "dependencies": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-hover": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-hover/-/has-hover-1.0.1.tgz", + "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/has-passive-events": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-passive-events/-/has-passive-events-1.0.0.tgz", + "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-browser/-/is-browser-2.1.0.tgz", + "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-firefox": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-firefox/-/is-firefox-1.0.3.tgz", + "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", + "license": "MIT" + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string-blank": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-string-blank/-/is-string-blank-1.0.1.tgz", + "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", + "license": "MIT" + }, + "node_modules/is-svg-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-svg-path/-/is-svg-path-1.0.2.tgz", + "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/map-limit": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", + "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", + "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", + "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-log2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", + "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mouse-change": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", + "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", + "license": "MIT", + "dependencies": { + "mouse-event": "^1.0.0" + } + }, + "node_modules/mouse-event": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/mouse-event/-/mouse-event-1.0.5.tgz", + "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", + "license": "MIT" + }, + "node_modules/mouse-event-offset": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", + "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", + "license": "MIT" + }, + "node_modules/mouse-wheel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mouse-wheel/-/mouse-wheel-1.2.0.tgz", + "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", + "license": "MIT", + "dependencies": { + "right-now": "^1.0.0", + "signum": "^1.0.0", + "to-px": "^1.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-svg-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", + "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", + "license": "MIT" + }, + "node_modules/number-is-integer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-integer/-/number-is-integer-1.0.1.tgz", + "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT" + }, + "node_modules/parse-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parse-rect/-/parse-rect-1.2.0.tgz", + "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", + "license": "MIT", + "dependencies": { + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parse-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-unit/-/parse-unit-1.0.1.tgz", + "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/pick-by-alias": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pick-by-alias/-/pick-by-alias-1.2.0.tgz", + "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plotly.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-3.5.0.tgz", + "integrity": "sha512-a3AYQIMG7OdZmrJ/fJ65HSt3g1l5qDeludKqjjafU1dh5E+fwqDhsEBndW7VCYwjlducCfN6KtPdWdiWFcoBWw==", + "license": "MIT", + "dependencies": { + "@plotly/d3": "3.8.2", + "@plotly/d3-sankey": "0.7.2", + "@plotly/d3-sankey-circular": "0.33.1", + "@plotly/mapbox-gl": "1.13.4", + "@plotly/regl": "^2.1.2", + "@turf/area": "^7.1.0", + "@turf/bbox": "^7.1.0", + "@turf/centroid": "^7.1.0", + "base64-arraybuffer": "^1.0.2", + "canvas-fit": "^1.5.0", + "color-alpha": "1.0.4", + "color-normalize": "1.5.0", + "color-parse": "2.0.0", + "color-rgba": "3.0.0", + "country-regex": "^1.1.0", + "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", + "d3-hierarchy": "^1.1.9", + "d3-interpolate": "^3.0.1", + "d3-time": "^1.1.0", + "d3-time-format": "^2.2.3", + "fast-isnumeric": "^1.1.4", + "gl-mat4": "^1.2.0", + "gl-text": "^1.4.0", + "has-hover": "^1.0.1", + "has-passive-events": "^1.0.0", + "is-mobile": "^4.0.0", + "maplibre-gl": "^4.7.1", + "mouse-change": "^1.4.0", + "mouse-event-offset": "^3.0.2", + "mouse-wheel": "^1.2.0", + "native-promise-only": "^0.8.1", + "parse-svg-path": "^0.1.2", + "point-in-polygon": "^1.1.0", + "polybooljs": "^1.2.2", + "probe-image-size": "^7.2.3", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.3", + "regl-scatter2d": "^3.3.1", + "regl-splom": "^1.0.14", + "strongly-connected-components": "^1.0.1", + "superscript-text": "^1.0.0", + "svg-path-sdf": "^1.1.3", + "tinycolor2": "^1.4.2", + "to-px": "1.0.1", + "topojson-client": "^3.1.0", + "webgl-context": "^2.2.0", + "world-calendars": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/polybooljs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/polybooljs/-/polybooljs-1.2.2.tgz", + "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/probe-image-size": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", + "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", + "license": "MIT", + "dependencies": { + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-plotly.js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz", + "integrity": "sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "plotly.js": ">1.34.0", + "react": ">0.13.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/regl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.1.tgz", + "integrity": "sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==", + "license": "MIT" + }, + "node_modules/regl-error2d": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/regl-error2d/-/regl-error2d-2.0.12.tgz", + "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-line2d": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.1.3.tgz", + "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-find-index": "^1.0.2", + "array-normalize": "^1.1.4", + "color-normalize": "^1.5.0", + "earcut": "^2.1.5", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0" + } + }, + "node_modules/regl-scatter2d": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.4.0.tgz", + "integrity": "sha512-DavKQlHsI+iHZuLgOL+yGkg+sPd94CS+7FCBWkcQ6s/TbaNfUsF9eN591fjjSWIoKrGNfb/SEGhsXR5lXjqZ2w==", + "license": "MIT", + "dependencies": { + "@plotly/point-cluster": "^3.1.9", + "array-bounds": "^1.0.1", + "color-id": "^1.1.0", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "glslify": "^7.0.0", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-splom": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.14.tgz", + "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-range": "^1.0.1", + "color-alpha": "^1.0.4", + "flatten-vertex-data": "^1.0.2", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "raf": "^3.4.1", + "regl-scatter2d": "^3.2.3" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/right-now": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", + "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/signum/-/signum-1.0.0.tgz", + "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", + "engines": { + "node": "*" + } + }, + "node_modules/static-eval": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", + "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", + "license": "MIT", + "dependencies": { + "escodegen": "^2.1.0" + } + }, + "node_modules/stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", + "license": "MIT", + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "dependencies": { + "parenthesis": "^3.1.5" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strongly-connected-components": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", + "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", + "license": "MIT" + }, + "node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "license": "ISC", + "dependencies": { + "kdbush": "^3.0.0" + } + }, + "node_modules/supercluster/node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", + "license": "ISC" + }, + "node_modules/superscript-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/superscript-text/-/superscript-text-1.0.0.tgz", + "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "node_modules/svg-path-bounds": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", + "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "^0.1.1", + "is-svg-path": "^1.0.1", + "normalize-svg-path": "^1.0.0", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/svg-path-sdf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", + "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", + "license": "MIT", + "dependencies": { + "bitmap-sdf": "^1.0.0", + "draw-svg-path": "^1.0.0", + "is-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2", + "svg-path-bounds": "^1.0.1" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, + "node_modules/to-float32": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-float32/-/to-float32-1.1.0.tgz", + "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", + "license": "MIT" + }, + "node_modules/to-px": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz", + "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", + "license": "MIT", + "dependencies": { + "parse-unit": "^1.0.1" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typedarray-pool": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typedarray-pool/-/typedarray-pool-1.2.0.tgz", + "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.0", + "dup": "^1.0.0" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-diff/-/update-diff-1.1.0.tgz", + "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/weak-map": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", + "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", + "license": "Apache-2.0" + }, + "node_modules/webgl-context": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz", + "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", + "license": "MIT", + "dependencies": { + "get-canvas-context": "^1.0.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/world-calendars": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.4.tgz", + "integrity": "sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..34298834 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.15.0", + "plotly.js": "^3.5.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-plotly.js": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.4" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 00000000..4fba4ed2 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,65 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: #121212; + color: #ffffff; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +.dashboard-container { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 80px; + height: 100vh; + width: 100vw; + gap: 10px; + padding: 10px; +} + +.panel { + background-color: #1e1e1e; + border-radius: 8px; + padding: 20px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.structure-panel { + grid-column: 1 / 2; + grid-row: 1 / 2; +} + +.trace-panel { + grid-column: 2 / 3; + grid-row: 1 / 2; + align-items: center; + justify-content: center; +} + +.timeline-panel { + grid-column: 1 / 3; + grid-row: 2 / 3; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +h2 { + font-size: 1.2rem; + margin-bottom: 10px; + color: #a0a0a0; +} + +.status-badge { + background-color: #2e7d32; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + margin-left: 10px; +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 00000000..67bdbd9e --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,177 @@ +import { useState, useEffect, useRef } from 'react' +import axios from 'axios' +import PlotComponent from 'react-plotly.js' +const Plot = PlotComponent.default || PlotComponent; +import './App.css' + +const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8000'; + +function App() { + const [meta, setMeta] = useState(null) + + // Trace Explorer State + const [viewMode, setViewMode] = useState('evolution') + const [iteration, setIteration] = useState(0) + const [layer, setLayer] = useState(0) + const [heatmapData, setHeatmapData] = useState([[0]]) + + // Structure Viewer State + const [pdbText, setPdbText] = useState(null) + const [renderStyle, setRenderStyle] = useState('cartoon') + const [colorMode, setColorMode] = useState('confidence') + const viewerRef = useRef(null) + + // NEW: State to track which amino acid you clicked + const [selectedResidue, setSelectedResidue] = useState(null) + + // 1. Fetch Meta and PDB on load + useEffect(() => { + axios.get(`${API_BASE}/meta`) + .then(response => setMeta(response.data)) + .catch(error => console.error("Error fetching meta:", error)) + + axios.get(`${API_BASE}/structure?t=${Date.now()}`) + .then(response => setPdbText(response.data)) + .catch(error => console.error("Error fetching PDB:", error)) + }, []) + + // 2. Dynamic Fetching based on View Mode + useEffect(() => { + if (!meta) return; + + if (viewMode === 'evolution') { + axios.get(`${API_BASE}/tensor/activations/recycle_${iteration}_s_z`) + .then(response => { + // Backend already averaged across channels + setHeatmapData(response.data.data); + }) + .catch(() => { + console.log(`No data for iteration ${iteration}`); + setHeatmapData([[0]]); + }) + } + else if (viewMode === 'attention') { + const layerStr = layer.toString().padStart(3, '0'); + axios.get(`${API_BASE}/tensor/attention/layer_${layerStr}`) + .then(response => { + // Backend already averaged across all attention heads + setHeatmapData(response.data.data); + }) + .catch(() => { + console.log(`No data for layer ${layerStr}`); + setHeatmapData([[0]]); + }) + } + }, [iteration, layer, viewMode, meta]) + + // 3. Render 3Dmol viewer + useEffect(() => { + if (pdbText && viewerRef.current && window.$3Dmol) { + viewerRef.current.innerHTML = ''; + let viewer = window.$3Dmol.createViewer(viewerRef.current, { backgroundColor: '#1e1e1e' }); + viewer.addModel(pdbText, "pdb"); + + let colorConfig = {}; + if (colorMode === 'spectrum') { + colorConfig = { color: 'spectrum' }; + } else if (colorMode === 'confidence') { + colorConfig = { colorscheme: { prop: 'b', gradient: 'rwb', min: 0.5, max: 1.0 } }; + } + + let styleObj = {}; + styleObj[renderStyle] = colorConfig; + viewer.setStyle({}, styleObj); + + viewer.setClickable({}, true, function(atom) { + viewer.removeAllLabels(); + viewer.addLabel(`${atom.resn} ${atom.resi}`, { + position: atom, backgroundColor: '#333333', fontColor: 'white', backgroundOpacity: 0.8 + }); + viewer.render(); + + // NEW: Tell React which residue number was clicked! + setSelectedResidue(atom.resi); + }); + + viewer.zoomTo(); + viewer.render(); + } + }, [pdbText, renderStyle, colorMode]) + + // NEW: Calculate the dynamic crosshair shapes for Plotly + let crosshairShapes = []; + if (selectedResidue && heatmapData.length > 1) { + const idx = selectedResidue - 1; // PDB is 1-indexed, JavaScript arrays are 0-indexed + const maxIdx = heatmapData.length - 1; + crosshairShapes = [ + { type: 'line', x0: idx, x1: idx, y0: 0, y1: maxIdx, line: { color: '#ff0055', width: 2, dash: 'dot' } }, // Vertical + { type: 'line', x0: 0, x1: maxIdx, y0: idx, y1: idx, line: { color: '#ff0055', width: 2, dash: 'dot' } } // Horizontal + ]; + } + + return ( +
+ +
+
+

Structure Viewer {meta && Model: {meta.model_name}}

+
+ + +
+
+
+
+ +
+
+

Trace Explorer

+ {/* NEW: Display the currently selected residue */} + {selectedResidue && Target: Residue {selectedResidue}} + +
+ + +
+ +
+ {viewMode === 'evolution' ? ( + <> +

Recycling Iterations

+ setIteration(parseInt(e.target.value))} style={{ width: '60%', margin: '0 20px' }} /> + Iteration: {iteration} + + ) : ( + <> +

ESM-2 Transformer Layers

+ setLayer(parseInt(e.target.value))} style={{ width: '60%', margin: '0 20px' }} /> + Layer: {layer} + + )} +
+ +
+ ) +} + +export default App \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 00000000..5101b674 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 00000000..2c84af06 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,111 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 00000000..3d9da8ac --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/server.py b/server.py new file mode 100644 index 00000000..006f14f3 --- /dev/null +++ b/server.py @@ -0,0 +1,102 @@ +import os +import json +import torch +import argparse +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse +from fastapi.middleware.cors import CORSMiddleware + +# --- CLI Argument Parsing --- +parser = argparse.ArgumentParser(description="VizFold API Bridge") +parser.add_argument( + "--dir", + type=str, + default="test_output", + help="Path to the inference output directory (default: test_output)" +) +# We use parse_known_args so it doesn't crash if run with external uvicorn flags +args, _ = parser.parse_known_args() + +# Lock down the base directory based on CLI input +BASE_DIR = os.path.abspath(args.dir) + +app = FastAPI(title="VizFold API Bridge") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +def safe_join(directory, *paths): + """Ensures requested files cannot escape the BASE_DIR.""" + requested_path = os.path.abspath(os.path.join(directory, *paths)) + if not requested_path.startswith(BASE_DIR): + raise HTTPException(status_code=403, detail="Path traversal attempt blocked.") + return requested_path + +@app.get("/") +def read_root(): + return {"status": "VizFold API is live", "directory": BASE_DIR} + +@app.get("/meta") +def get_meta(): + meta_path = safe_join(BASE_DIR, "meta.json") + if not os.path.exists(meta_path): + raise HTTPException(status_code=404, detail="meta.json not found") + with open(meta_path, "r") as f: + return json.load(f) + +@app.get("/structure") +def get_structure(): + pdb_path = safe_join(BASE_DIR, "structure", "predicted.pdb") + if not os.path.exists(pdb_path): + raise HTTPException(status_code=404, detail="PDB structure not found") + return FileResponse(pdb_path, media_type="text/plain") + +@app.get("/tensor/{category}/{filename}") +def get_tensor(category: str, filename: str): + # 1. Explicitly block path traversal characters in the filename or category + if ".." in filename or "/" in filename or "\\" in filename: + raise HTTPException(status_code=400, detail="Invalid characters in filename") + if ".." in category or "/" in category or "\\" in category: + raise HTTPException(status_code=400, detail="Invalid characters in category") + + # 2. Security check: only allow reading from specific trace directories + if category not in ["attention", "activations", "structure_module"]: + raise HTTPException(status_code=400, detail="Invalid trace category") + + filepath = safe_join(BASE_DIR, "trace", category, f"{filename}.pt") + + if not os.path.exists(filepath): + raise HTTPException(status_code=404, detail=f"Tensor {filename}.pt not found") + + try: + tensor = torch.load(filepath, weights_only=True) + tensor = tensor.float().detach().cpu() + + # 1. Average Trunk Evolution (s_z) across the 128 hidden channels + if category == "activations" and "s_z" in filename: + tensor = tensor.mean(dim=-1) + + # 2. Average ESM-2 Attention across all Attention Heads + elif category == "attention" and "layer" in filename: + if tensor.dim() == 4: + tensor = tensor.squeeze(0) + tensor = tensor.mean(dim=0) + + tensor_list = tensor.numpy().tolist() + + return { + "name": filename, + "shape": list(tensor.shape), + "data": tensor_list + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error processing tensor: {str(e)}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/vizfold/backends/esmfold/hooks.py b/vizfold/backends/esmfold/hooks.py index 1e05fd7f..23b3586b 100644 --- a/vizfold/backends/esmfold/hooks.py +++ b/vizfold/backends/esmfold/hooks.py @@ -293,6 +293,28 @@ def hook(module: nn.Module, inp: Any, out: Any) -> None: self.trunk_blocks[f"block_{block_idx:03d}_pair"] = pair_state.squeeze(0).detach().cpu() return hook + def _make_trunk_hook(self) -> Callable: + """Hook that captures s_s and s_z at every recycling iteration.""" + def hook(module: nn.Module, inp: Any, out: Any) -> None: + s_s, s_z = None, None + + # Handle HF returning a tuple (usually s_s is index 0 and s_z is index 1) + if isinstance(out, tuple) and len(out) >= 2: + s_s, s_z = out[0], out[1] + # Handle HF returning a dataclass or object + elif hasattr(out, 's_s') and hasattr(out, 's_z'): + s_s, s_z = out.s_s, out.s_z + # Handle dictionaries + elif isinstance(out, dict): + s_s, s_z = out.get('s_s'), out.get('s_z') + + if s_s is not None and s_z is not None: + # Squeeze out the batch dimension and move to CPU to prevent RAM crashes + # s_s shape: [N, 1024] | s_z shape: [N, N, 128] + self.recycled_s_s.append(s_s.squeeze(0).cpu().detach()) + self.recycled_s_z.append(s_z.squeeze(0).cpu().detach()) + return hook + class StructureModuleTraceCollector: """ diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index a1d32462..256aa57e 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -269,10 +269,35 @@ def log(msg: str) -> None: if trace_mode != "none": collector.remove_hooks() + # Archive the recycled s_s and s_z tensors we caught + if want_act and len(collector.recycled_s_s) > 0: + num_iters = len(collector.recycled_s_s) + log(f"[{self.model_name}] [{trace_mode}] Captured {num_iters} trunk recycling iterations.") + for i in range(num_iters): + collector.activations[f"recycle_{i}_s_s"] = collector.recycled_s_s[i] + collector.activations[f"recycle_{i}_s_z"] = collector.recycled_s_z[i] + + # out.s_s: folding trunk single representations [B, N, 1024]. + # These are the per-residue embeddings produced by ESMFold's + # structure module, complementing the ESM-2 encoder traces. + if hasattr(out, 's_s') and out.s_s is not None: + single_reps = out.s_s.squeeze(0).cpu() + log(f"[{self.model_name}] [{trace_mode}] Extracted folding trunk s_s: {single_reps.shape}") + else: + log(f"[{self.model_name}] [{trace_mode}] out.s_s not found — folding trunk single representations missing.") + if sm_collector is not None: sm_collector.remove_hooks() log(f"Structure module traces: {len(sm_collector.ipa_attention)} IPA blocks, " f"{len(sm_collector.backbone_positions)} recycle iterations.") + # Check if the output object contains the single representations + if hasattr(out, 's_s') and out.s_s is not None: + # Move to CPU and remove the batch dimension -> [seq_len, hidden_dim] + single_reps = out.s_s.squeeze(0).cpu() + + log(f"Extracted folding trunk s_s activations: {single_reps.shape}") + else: + log("Warning: out.s_s not found. Folding trunk single representations missing.") log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " f"{len(collector.activations)} activation layers.") diff --git a/vizfold/backends/esmfold/requirements.txt b/vizfold/backends/esmfold/requirements.txt new file mode 100644 index 00000000..97dc7cd8 --- /dev/null +++ b/vizfold/backends/esmfold/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn From 65245e1c4a47b774408a02e76cb00aa425e6d8e0 Mon Sep 17 00:00:00 2001 From: rohan5986 <136632833+rohan5986@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:22:24 -0400 Subject: [PATCH 17/18] Removed duplicate extraction logic, fixed scaffold CSS, and deleted unused SVGs (#8) * Extract s_s folding trunk activations and enforce safetensors * Update backend pipeline * Capture s_s and s_z at every recycling iteration via trunk hook * Remove test output artifacts * Complete interactive vizfold dashboard with 3Dmol and attention heatmaps * add frontend package dependencies * fix: address final security reviews and add frontend env configuration * add fastapi and uvicorn to backend requirements * Removed duplicate extraction logic, fixed scaffold CSS, and deleted unused SVGs * fix: re-apply SVG deletions and duplicate block removal --------- Co-authored-by: Rohan Singhal --- frontend/public/icons.svg | 24 ------ frontend/src/assets/react.svg | 1 - frontend/src/assets/vite.svg | 1 - frontend/src/index.css | 108 -------------------------- vizfold/backends/esmfold/inference.py | 8 -- 5 files changed, 142 deletions(-) delete mode 100644 frontend/public/icons.svg delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/assets/vite.svg diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg deleted file mode 100644 index e9522193..00000000 --- a/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b674..00000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/index.css b/frontend/src/index.css index 2c84af06..293d3b1f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,111 +1,3 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - body { margin: 0; } - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} diff --git a/vizfold/backends/esmfold/inference.py b/vizfold/backends/esmfold/inference.py index 256aa57e..5da39b0a 100644 --- a/vizfold/backends/esmfold/inference.py +++ b/vizfold/backends/esmfold/inference.py @@ -290,14 +290,6 @@ def log(msg: str) -> None: sm_collector.remove_hooks() log(f"Structure module traces: {len(sm_collector.ipa_attention)} IPA blocks, " f"{len(sm_collector.backbone_positions)} recycle iterations.") - # Check if the output object contains the single representations - if hasattr(out, 's_s') and out.s_s is not None: - # Move to CPU and remove the batch dimension -> [seq_len, hidden_dim] - single_reps = out.s_s.squeeze(0).cpu() - - log(f"Extracted folding trunk s_s activations: {single_reps.shape}") - else: - log("Warning: out.s_s not found. Folding trunk single representations missing.") log(f"Forward pass complete. Captured {len(collector.attention)} attention layers, " f"{len(collector.activations)} activation layers.") From f491ef76d59ae6510ff2b964901247b39968fa8f Mon Sep 17 00:00:00 2001 From: Jayanth Vennamreddy <53269831+jayvenn21@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:36:38 -0400 Subject: [PATCH 18/18] Refactor frontend viz: responsive heatmap, camera persistence, bidirectional sync (#9) Split monolithic App.jsx into StructureViewer, TraceExplorer, and TimelineControls components. Preserve 3Dmol camera on style/color changes by separating viewer creation from style application. Make Plotly heatmap responsive (autosize) instead of fixed 500x500. Add bidirectional crosshair sync (heatmap click highlights residue in 3D). Add colorbar labels per view mode and loading/error states per panel. Extract all inline styles to CSS classes. --- frontend/src/App.css | 84 +++++++- frontend/src/App.jsx | 203 +++++++------------ frontend/src/components/StructureViewer.jsx | 83 ++++++++ frontend/src/components/TimelineControls.jsx | 33 +++ frontend/src/components/TraceExplorer.jsx | 57 ++++++ 5 files changed, 329 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/StructureViewer.jsx create mode 100644 frontend/src/components/TimelineControls.jsx create mode 100644 frontend/src/components/TraceExplorer.jsx diff --git a/frontend/src/App.css b/frontend/src/App.css index 4fba4ed2..a8b7f0f0 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -37,8 +37,6 @@ body { .trace-panel { grid-column: 2 / 3; grid-row: 1 / 2; - align-items: center; - justify-content: center; } .timeline-panel { @@ -55,6 +53,31 @@ h2 { color: #a0a0a0; } +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.panel-header h2 { + margin: 0; +} + +.controls-group { + display: flex; + gap: 10px; +} + +.dropdown { + padding: 4px 8px; + background-color: #333; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + .status-badge { background-color: #2e7d32; color: white; @@ -62,4 +85,59 @@ h2 { border-radius: 4px; font-size: 0.8rem; margin-left: 10px; -} \ No newline at end of file +} + +.viewer-container { + flex: 1; + position: relative; + border: 1px solid #333; + border-radius: 4px; +} + +.residue-target { + color: #ff0055; + font-weight: bold; +} + +.slider { + width: 60%; + margin: 0 20px; +} + +.trace-plot { + flex: 1; + width: 100% !important; + height: 100% !important; + min-height: 0; +} + +.panel-message { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: #a0a0a0; + font-size: 0.95rem; +} + +.panel-message.error { + color: #ef5350; +} + +.error-panel { + grid-column: 1 / 3; + grid-row: 1 / 3; + align-items: center; + justify-content: center; +} + +.error-message { + color: #ef5350; + font-size: 1.2rem; + margin-bottom: 8px; +} + +.error-hint { + color: #a0a0a0; + font-family: monospace; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 67bdbd9e..8971a110 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,177 +1,124 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useCallback } from 'react' import axios from 'axios' -import PlotComponent from 'react-plotly.js' -const Plot = PlotComponent.default || PlotComponent; +import StructureViewer from './components/StructureViewer' +import TraceExplorer from './components/TraceExplorer' +import TimelineControls from './components/TimelineControls' import './App.css' const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8000'; function App() { const [meta, setMeta] = useState(null) - - // Trace Explorer State - const [viewMode, setViewMode] = useState('evolution') + const [metaError, setMetaError] = useState(null) + + const [pdbText, setPdbText] = useState(null) + const [structureError, setStructureError] = useState(null) + + const [viewMode, setViewMode] = useState('evolution') const [iteration, setIteration] = useState(0) const [layer, setLayer] = useState(0) - const [heatmapData, setHeatmapData] = useState([[0]]) - - // Structure Viewer State - const [pdbText, setPdbText] = useState(null) - const [renderStyle, setRenderStyle] = useState('cartoon') - const [colorMode, setColorMode] = useState('confidence') - const viewerRef = useRef(null) + const [heatmapData, setHeatmapData] = useState(null) + const [heatmapLoading, setHeatmapLoading] = useState(false) + + const [renderStyle, setRenderStyle] = useState('cartoon') + const [colorMode, setColorMode] = useState('confidence') - // NEW: State to track which amino acid you clicked const [selectedResidue, setSelectedResidue] = useState(null) - // 1. Fetch Meta and PDB on load useEffect(() => { axios.get(`${API_BASE}/meta`) - .then(response => setMeta(response.data)) - .catch(error => console.error("Error fetching meta:", error)) + .then(r => setMeta(r.data)) + .catch(() => setMetaError('Could not connect to backend')) axios.get(`${API_BASE}/structure?t=${Date.now()}`) - .then(response => setPdbText(response.data)) - .catch(error => console.error("Error fetching PDB:", error)) + .then(r => setPdbText(r.data)) + .catch(() => setStructureError('Failed to load PDB structure')) }, []) - // 2. Dynamic Fetching based on View Mode useEffect(() => { - if (!meta) return; - - if (viewMode === 'evolution') { - axios.get(`${API_BASE}/tensor/activations/recycle_${iteration}_s_z`) - .then(response => { - // Backend already averaged across channels - setHeatmapData(response.data.data); - }) - .catch(() => { - console.log(`No data for iteration ${iteration}`); - setHeatmapData([[0]]); - }) - } - else if (viewMode === 'attention') { - const layerStr = layer.toString().padStart(3, '0'); - axios.get(`${API_BASE}/tensor/attention/layer_${layerStr}`) - .then(response => { - // Backend already averaged across all attention heads - setHeatmapData(response.data.data); - }) - .catch(() => { - console.log(`No data for layer ${layerStr}`); - setHeatmapData([[0]]); - }) - } + if (!meta) return + setHeatmapLoading(true) + + const url = viewMode === 'evolution' + ? `${API_BASE}/tensor/activations/recycle_${iteration}_s_z` + : `${API_BASE}/tensor/attention/layer_${layer.toString().padStart(3, '0')}` + + axios.get(url) + .then(r => setHeatmapData(r.data.data)) + .catch(() => setHeatmapData(null)) + .finally(() => setHeatmapLoading(false)) }, [iteration, layer, viewMode, meta]) - // 3. Render 3Dmol viewer - useEffect(() => { - if (pdbText && viewerRef.current && window.$3Dmol) { - viewerRef.current.innerHTML = ''; - let viewer = window.$3Dmol.createViewer(viewerRef.current, { backgroundColor: '#1e1e1e' }); - viewer.addModel(pdbText, "pdb"); - - let colorConfig = {}; - if (colorMode === 'spectrum') { - colorConfig = { color: 'spectrum' }; - } else if (colorMode === 'confidence') { - colorConfig = { colorscheme: { prop: 'b', gradient: 'rwb', min: 0.5, max: 1.0 } }; - } - - let styleObj = {}; - styleObj[renderStyle] = colorConfig; - viewer.setStyle({}, styleObj); - - viewer.setClickable({}, true, function(atom) { - viewer.removeAllLabels(); - viewer.addLabel(`${atom.resn} ${atom.resi}`, { - position: atom, backgroundColor: '#333333', fontColor: 'white', backgroundOpacity: 0.8 - }); - viewer.render(); - - // NEW: Tell React which residue number was clicked! - setSelectedResidue(atom.resi); - }); - - viewer.zoomTo(); - viewer.render(); - } - }, [pdbText, renderStyle, colorMode]) - - // NEW: Calculate the dynamic crosshair shapes for Plotly - let crosshairShapes = []; - if (selectedResidue && heatmapData.length > 1) { - const idx = selectedResidue - 1; // PDB is 1-indexed, JavaScript arrays are 0-indexed - const maxIdx = heatmapData.length - 1; - crosshairShapes = [ - { type: 'line', x0: idx, x1: idx, y0: 0, y1: maxIdx, line: { color: '#ff0055', width: 2, dash: 'dot' } }, // Vertical - { type: 'line', x0: 0, x1: maxIdx, y0: idx, y1: idx, line: { color: '#ff0055', width: 2, dash: 'dot' } } // Horizontal - ]; + const handleResidueClick = useCallback((resi) => { + setSelectedResidue(resi) + }, []) + + if (metaError) { + return ( +
+
+

{metaError}

+

Start the backend: python3 server.py

+
+
+ ) } return (
-
-
-

Structure Viewer {meta && Model: {meta.model_name}}

-
- setRenderStyle(e.target.value)}> - setColorMode(e.target.value)}>
-
+
-
-

Trace Explorer

- {/* NEW: Display the currently selected residue */} - {selectedResidue && Target: Residue {selectedResidue}} - setViewMode(e.target.value)}>
- -
-
- {viewMode === 'evolution' ? ( - <> -

Recycling Iterations

- setIteration(parseInt(e.target.value))} style={{ width: '60%', margin: '0 20px' }} /> - Iteration: {iteration} - - ) : ( - <> -

ESM-2 Transformer Layers

- setLayer(parseInt(e.target.value))} style={{ width: '60%', margin: '0 20px' }} /> - Layer: {layer} - - )} -
- +
) } -export default App \ No newline at end of file +export default App diff --git a/frontend/src/components/StructureViewer.jsx b/frontend/src/components/StructureViewer.jsx new file mode 100644 index 00000000..fd8369c2 --- /dev/null +++ b/frontend/src/components/StructureViewer.jsx @@ -0,0 +1,83 @@ +import { useEffect, useRef } from 'react' + +function applyStyle(viewer, renderStyle, colorMode) { + const colorConfig = colorMode === 'spectrum' + ? { color: 'spectrum' } + : { colorscheme: { prop: 'b', gradient: 'rwb', min: 0.5, max: 1.0 } } + viewer.setStyle({}, { [renderStyle]: colorConfig }) +} + +export default function StructureViewer({ pdbText, renderStyle, colorMode, selectedResidue, onResidueClick, error }) { + const containerRef = useRef(null) + const viewerRef = useRef(null) + const zoomedRef = useRef(false) + const internalClickRef = useRef(null) + + // Create viewer once when PDB data arrives + useEffect(() => { + if (!pdbText || !containerRef.current || !window.$3Dmol) return + + containerRef.current.innerHTML = '' + const viewer = window.$3Dmol.createViewer(containerRef.current, { backgroundColor: '#1e1e1e' }) + viewer.addModel(pdbText, 'pdb') + viewerRef.current = viewer + zoomedRef.current = false + + viewer.setClickable({}, true, (atom) => { + viewer.removeAllLabels() + viewer.addLabel(`${atom.resn} ${atom.resi}`, { + position: atom, + backgroundColor: '#333333', + fontColor: 'white', + backgroundOpacity: 0.8, + }) + viewer.render() + internalClickRef.current = atom.resi + onResidueClick(atom.resi) + }) + + return () => { viewerRef.current = null } + }, [pdbText, onResidueClick]) + + // Apply rendering style without recreating the viewer (preserves camera) + useEffect(() => { + const v = viewerRef.current + if (!v) return + applyStyle(v, renderStyle, colorMode) + if (!zoomedRef.current) { + v.zoomTo() + zoomedRef.current = true + } + v.render() + }, [pdbText, renderStyle, colorMode]) + + // Highlight residue selected from the heatmap + useEffect(() => { + const viewer = viewerRef.current + if (!viewer || selectedResidue == null) return + + // Skip if this selection originated from our own click handler + if (internalClickRef.current === selectedResidue) { + internalClickRef.current = null + return + } + + viewer.removeAllLabels() + const atoms = viewer.selectedAtoms({ resi: selectedResidue, atom: 'CA' }) + if (atoms.length > 0) { + const a = atoms[0] + viewer.addLabel(`${a.resn} ${a.resi}`, { + position: a, + backgroundColor: '#333333', + fontColor: 'white', + backgroundOpacity: 0.8, + }) + } + viewer.render() + }, [selectedResidue]) + + if (error) return
{error}
+ if (!pdbText) return
Loading structure…
+ + return
+} diff --git a/frontend/src/components/TimelineControls.jsx b/frontend/src/components/TimelineControls.jsx new file mode 100644 index 00000000..49414c1c --- /dev/null +++ b/frontend/src/components/TimelineControls.jsx @@ -0,0 +1,33 @@ +export default function TimelineControls({ viewMode, iteration, layer, meta, onIterationChange, onLayerChange }) { + return ( +
+ {viewMode === 'evolution' ? ( + <> +

Recycling Iterations

+ onIterationChange(parseInt(e.target.value))} + /> + Iteration: {iteration} + + ) : ( + <> +

ESM-2 Transformer Layers

+ onLayerChange(parseInt(e.target.value))} + /> + Layer: {layer} + + )} +
+ ) +} diff --git a/frontend/src/components/TraceExplorer.jsx b/frontend/src/components/TraceExplorer.jsx new file mode 100644 index 00000000..4e31b4ec --- /dev/null +++ b/frontend/src/components/TraceExplorer.jsx @@ -0,0 +1,57 @@ +import PlotComponent from 'react-plotly.js' +const Plot = PlotComponent.default || PlotComponent + +const COLORBAR_TITLES = { + evolution: 'Avg. Pair Repr.', + attention: 'Avg. Attention Weight', +} + +export default function TraceExplorer({ heatmapData, viewMode, selectedResidue, onResidueClick, loading }) { + let crosshairShapes = [] + if (selectedResidue && heatmapData && heatmapData.length > 1) { + const idx = selectedResidue - 1 + const maxIdx = heatmapData.length - 1 + crosshairShapes = [ + { type: 'line', x0: idx, x1: idx, y0: 0, y1: maxIdx, line: { color: '#ff0055', width: 2, dash: 'dot' } }, + { type: 'line', x0: 0, x1: maxIdx, y0: idx, y1: idx, line: { color: '#ff0055', width: 2, dash: 'dot' } }, + ] + } + + const handleClick = (event) => { + if (event.points && event.points.length > 0) { + onResidueClick(event.points[0].x + 1) + } + } + + if (loading) return
Loading trace data…
+ if (!heatmapData) return
No trace data available
+ + return ( + + ) +}