diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 0aaca09..b613854 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,26 +1,58 @@
-name: Publish to PyPI
+name: Create GitHub Release
on:
- release:
- types: [ created ]
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: write
jobs:
- build-and-publish:
+ release:
runs-on: ubuntu-latest
+
steps:
- - uses: actions/checkout@v4
+ - name: Checkout repository
+ uses: actions/checkout@v4
- - name: Set up Python
- uses: actions/setup-python@v4
+ - name: Set up Pixi
+ uses: prefix-dev/setup-pixi@v0.9.3
with:
- python-version: '3.x'
+ pixi-version: latest
+ cache: true
+
+ - name: Install Pixi environment
+ run: pixi install
+
+ - name: Run tests
+ run: |
+ pixi run test || echo "No Pixi test task configured; skipping tests."
+
+ - name: Create release archive
+ run: |
+ mkdir -p release_assets
+
+ git archive \
+ --format=tar.gz \
+ --prefix=phoskintime-${GITHUB_REF_NAME}/ \
+ -o release_assets/phoskintime-${GITHUB_REF_NAME}.tar.gz \
+ HEAD
- - name: Install Poetry
+ - name: Copy reproducibility files
run: |
- curl -sSL https://install.python-poetry.org | python3 -
- echo "$HOME/.local/bin" >> $GITHUB_PATH
+ cp pixi.toml release_assets/
+ if [ -f pixi.lock ]; then cp pixi.lock release_assets/; fi
+ if [ -f README.md ]; then cp README.md release_assets/; fi
+ if [ -f LICENSE ]; then cp LICENSE release_assets/; fi
+ if [ -f CITATION.cff ]; then cp CITATION.cff release_assets/; fi
+ if [ -f .zenodo.json ]; then cp .zenodo.json release_assets/; fi
- - name: Build and publish
+ - name: Create GitHub release and upload assets
+ env:
+ GH_TOKEN: ${{ github.token }}
run: |
- poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
- poetry publish --build
\ No newline at end of file
+ gh release create "$GITHUB_REF_NAME" \
+ release_assets/* \
+ --title "$GITHUB_REF_NAME" \
+ --generate-notes
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bc99996..3c4471e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -5,26 +5,34 @@ on:
branches: [main]
pull_request:
+permissions:
+ contents: read
+
jobs:
test:
runs-on: ubuntu-latest
+
steps:
- name: Checkout repository
uses: actions/checkout@v5
- - name: Set up Python
- uses: actions/setup-python@v5
+ - name: Set up Pixi
+ uses: prefix-dev/setup-pixi@v0.9.6
+ with:
+ pixi-version: latest
+ cache: true
+
+ - name: Install dev environment
+ run: pixi install -e dev
+
+ - name: Run tests with coverage
+ run: pixi run -e dev coverage-all
+
+ - name: Upload coverage reports
+ uses: actions/upload-artifact@v4
+ if: always()
with:
- python-version: "3.12"
-
- - name: Install test and notebook dependencies
- run: |
- python -m pip install --upgrade pip
- python -m pip install \
- numpy pandas scipy scikit-learn statsmodels numba matplotlib seaborn plotly openpyxl xlsxwriter adjustText \
- jax jaxopt diffrax numpyro networkx graphviz mygene tqdm python-dotenv typer jinja2 tomli SALib \
- pytest pytest-cov nbmake ipykernel jupyter nbformat nbclient
-
- - name: Run tests including educational notebooks
- run: |
- PYTHONPATH=.:.. pytest
+ name: coverage-reports
+ path: |
+ coverage.xml
+ docs/assets/coverage.svg
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2d16db1..d989501 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,3 +64,9 @@ site/
!/notebooks/outputs/
!/notebooks/outputs/*/
!/notebooks/outputs/*/.gitkeep
+
+# Dashboard uploads and local archives
+/dashboard_uploads/
+*.zip
+/results/
+/.streamlit/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cb86e5..ae147cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,30 @@ All notable changes to this project are documented here.
- Workers in sensitivity simulations
+- Equations in notebooks
+
+- Console display, posterior log, workflow testing for frontend
+
+- Fixed the frontend no code dashboard, testing networmodel and protwise done
+
+- Cached rhs in model ode solving, and forward simulation script
+
+- Formatting python code
+
+- Directories path
+
+- Posterior params conversion fix
+
+- Network sweep and forward simulation
+
+- Phosphosite dynamics 4th panel in forward simulation
+
+- Phosphosite dynamics 4th panel in forward simulation & data fit inspector
+
+- Combinatorial model state phase explosion down to O(1) runtime, no memory explosion in for loop, vectorized RHS for combinatorial model.
+
+- Ambiguity in model loading for forward simulation-KO/WT dashboard
+
### Maintenance
@@ -45,6 +69,10 @@ All notable changes to this project are documented here.
- Add community, governance, citation, and repository-maintenance files
+- Update changelog and docs config file
+
+- Chore and fix: cleanup config file, fix mrna modality missing scenario, tested all scenarios, works!
+
### Other
@@ -594,6 +622,110 @@ Migrate networkmodel scalar path to JAX/Diffrax/JAXopt and update docs/CLI
- Figures dpi set to 300
+- Update changelog and documentation
+
+- Update README.md
+
+- Add notebook readiness tests across modules
+
+- Revert KinOpt and TFOpt notebook refactor
+
+- Validate networkmodel slice layouts exactly
+
+- Merge pull request #63 from bibymaths/codex/fix-failing-tests-and-increase-coverage
+
+Introduce notebook-safe projected finite-difference optimizers, lightweight LinearConstraint, tests and docs updates
+
+- Added coverage badge, fixed slice bounds tests, removed old pypi README.md
+
+- Dead scripts for thermal extension
+
+- Add executable educational notebooks
+
+- Add SALib to CI test dependencies
+
+- Merge pull request #64 from bibymaths/codex/create-educational-jupyter-notebooks-for-modules
+
+Add executable educational notebooks and CI/tests for KinOpt, TFOpt, protwise, and networkmodel
+
+- Pixi and README.md
+
+- Merge pull request #65 from bibymaths/missing-data-scenarios
+
+Missing data scenarios
+
+- Frontend plan
+
+- Frontend plan renamed
+
+- Standardize dashboard-ready output contract
+
+- Add dashboard result browser
+
+- Add dashboard CLI launcher
+
+- Add dashboard upload and configuration UI
+
+- Integrate workflow-specific dashboard panels
+
+- Add dashboard tests and documentation
+
+- Fix networkmodel custom config handling
+
+- Fix dashboard input extension validation
+
+- Fix ProtWise custom config handling
+
+- Add dashboard user, developer, and troubleshooting guides
+
+- Fix dashboard result discovery for reports and child runs
+
+- Restrict dashboard config inputs to TOML
+
+- Serialize run metadata with JSON-safe arrays
+
+- Fix advanced analysis dashboard commands
+
+- Merge pull request #67 from bibymaths/codex/standardize-dashboard-ready-output-contract
+
+Standardize dashboard-ready output contract
+
+- Add backward alias for old pymooo results to display in legacy mode and added imageio and gravis libs, module levelling for scripts
+
+- Density plots for posterior with gaussian smoothing
+
+- Fix forward phosphosite ODE panel
+
+- Merge pull request #68 from bibymaths/codex/fix-phosphosite-state-dynamics-panel
+
+Fix forward phosphosite ODE panel
+
+- Memory issue fixing
+
+- Make combinatorial model memory safe
+
+- Fix combinatorial S-rate export cache shape
+
+- Merge pull request #69 from bibymaths/codex/implement-memory-safe-fixes-for-combinatorial-model
+
+Make combinatorial model memory safe
+
+- Made a project presentation using the sample results.
+
+- Changelog, organic testing, publishing workflow for github and zenodo
+
+- Defer ODE input validation until runtime
+
+- Make imports safe with empty output paths
+
+- Merge pull request #70 from bibymaths/codex/fix-import-time-issues-with-empty-config.toml
+
+Defer ODE input validation until ProtWise runtime
+
+- Coverage, update readme with workflow, extension planning for future
+
+- Bump to v0.5.0
+
### Tests
@@ -605,6 +737,8 @@ Migrate networkmodel scalar path to JAX/Diffrax/JAXopt and update docs/CLI
- Testing network model with posterior sampling, removed history video
+- Notebooks are working
+
## [0.4.0] - 2025-05-06
diff --git a/README.md b/README.md
index e84b5cf..ca2f5cc 100644
--- a/README.md
+++ b/README.md
@@ -1,75 +1,63 @@

- [](https://doi.org/10.5281/zenodo.15351017)
- 
- 
- 
+[](https://doi.org/10.5281/zenodo.15351017)
+
+
+
---
-> 💡 **PhosKinTime** is an ODE-based modeling package for analyzing phosphorylation dynamics over time. It integrates
-parameter estimation, sensitivity analysis, steady-state computation, and visualization tools to help researchers
-explore kinase-substrate interactions in a temporal context.
+> 💡 **PhosKinTime** is an ODE-based modeling framework for analyzing phosphorylation dynamics over time. It integrates
+> parameter estimation, sensitivity analysis, steady-state computation, and visualization tools to help researchers
+> explore kinase-substrate interactions in a temporal context.
---
-## [Documentation](https://bibymaths.github.io/phoskintime/)
-
----
-
-
-
-🚧 Work in Progress: Web Interface & Package Finalization (Click to expand)
-
-
-I am actively working to make these powerful network dynamics tools as accessible and adaptable as possible for all researchers.
-
-Currently, I am focusing on two major updates:
-
-1. **A No-Code Web Interface:** I am building a frontend that will allow you to upload data, run parameter estimations, and generate interactive visualizations (PCA, t-SNE, network topology) directly from your browser—no command line required.
-2. **Finalizing the Python Package:** The core ODE modeling and optimization logic is built. I am currently finalizing the documentation for experimental data preparation and refining the command-line entry points to make the package easily adaptable to your specific datasets.
-
-*Note: Whether you prefer running Python scripts or using a graphical web app, be sure to **"Watch"** this repository (click the star/watch button at the top right) to be notified as soon as the complete documentation and frontend are released!*
-
+## [Documentation](https://bibymaths.github.io/phoskintime/)
---
## The Problem: Phosphorylation Cascades
-In cellular signaling pathways, a series of proteins are phosphorylated in an activation cascade that drives cellular responses. Understanding these post-translational modifications is critical.
+In cellular signaling pathways, a series of proteins are phosphorylated in an activation cascade that drives cellular
+responses. Understanding these post-translational modifications is critical.

-*Figure 1: Overview of protein post-translational modifications and the phosphorylation cascade mechanism.*
-
---
-## Features & Analysis
-PhosKinTime allows you to visualize network topology, track protein signal loss/propagation over time, and evaluate model convergence.
+## Workflow
-
+
-*Figure 2: PhosKinTime outputs including network graphing, kinetic time-series modeling, and residual analysis.*
----
+---
-Acknowledgements (Click to expand)
+Acknowledgements (Click to expand)
-This project originated as part of my master's thesis work at Theoretical Biophysics group (
-now, [Klipp-Linding Lab](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/)), Humboldt Universität zu Berlin.
+This project began from my master's thesis work in the Theoretical Biophysics group, now
+the [Klipp-Linding Lab](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/), at Humboldt-Universität zu Berlin. The
+submitted thesis focused on `kinopt`, while related components of the broader modelling framework were developed in
+parallel during that period and continued afterwards.
-- **Conceptual framework and mathematical modeling** were developed under the supervision of **[Prof. Dr. Dr. H.C. Edda Klipp](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/people/51-people/head/52-klipp)**.
-- **Experimental datasets** were provided by the **[(Retd. Prof.) Dr. Rune Linding](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/people/51-people/head/278-rune-linding)**.
-- The subpackage `tfopt` is an optimized and efficient derivative
- of [original work](https://github.com/Normann-BPh/Transcription-Optimization) by my colleague **[Julius Normann](https://github.com/Normann-BPh)**, adapted with permission.
+The implemented version of `kinopt` was developed as the core thesis-stage component. The `tfopt` and `protwise`
+components were developed by me during the same broader project period and subsequent continuation of the work. The
+`networkmodel` component was later designed and implemented independently after that period as an extension of the
+original modelling framework.
-I am especially grateful
-to [Ivo Maintz](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/people/54-people/6-staff/60-maintz) for his generous
-technical support, enabling seamless experimentation with packages and server setups.
+The initial distributive, successive, and combinatorial ODE formulations came from the thesis-stage modelling framework,
+while the saturation model was added independently at a later stage as an extension of the system.
+
+The subpackage `tfopt` is an optimized and extended derivative
+of [original work](https://github.com/Normann-BPh/Transcription-Optimization) by my
+colleague [Julius Normann](https://github.com/Normann-BPh), adapted with permission.
+
+I am grateful to [Ivo Maintz](https://rumo.biologie.hu-berlin.de/tbp/index.php/en/people/54-people/6-staff/60-maintz)
+for generous technical support with server access, package experimentation, and computational setup.
@@ -82,32 +70,23 @@ mechanistic hypotheses, including:
- **Distributive Model:** Phosphorylation events occur independently.
- **Successive Model:** Phosphorylation events occur sequentially.
-- **Random Model:** Phosphorylation events occur in a random manner.
-
-The package is designed with modularity in mind. It consists of several key components:
-
-- **Configuration:** Centralized settings (paths, parameter bounds, logging, etc.) are defined in the config module.
-- **Models:** Different ODE models (distributive, successive, random) are implemented to simulate phosphorylation.
-- **Parameter Estimation:** Normal estimation routines (`paramest/normest.py`) estimate kinetic parameters from
- experimental data.
-- **Sensitivity Analysis:** Morris sensitivity analysis is used to evaluate the influence of each parameter on the model
- output.
-- **Steady-State Calculation:** Functions compute steady-state initial conditions for ODE simulation.
-- **Utilities:** Helper functions support file handling, data formatting, report generation, and more.
-- **Visualization:** A comprehensive plotting module generates static and interactive plots to visualize model fits,
- parameter profiles, PCA, t-SNE, and sensitivity indices.
+- **Combinatorial Model:** Phosphorylation events occur in a hypercube lattice manner.
+- **Saturation Model:** Phosphorylation dynamics follow saturating kinetics, where phosphorylation rates approach an
+ upper limit as kinase input or substrate availability increases.
---
## Educational notebooks
-The repository includes executable educational Jupyter notebooks that demonstrate the core modeling workflows with tiny deterministic dummy data. They are designed for learning, CI execution, and quick smoke-testing of the current JAX/JAXopt/Diffrax implementation.
+The repository includes executable educational Jupyter notebooks that demonstrate the core modeling workflows with tiny
+deterministic dummy data. They are designed for learning, CI execution, and quick smoke-testing of the current
+JAX/JAXopt/Diffrax implementation.
-| Notebook | Module | What it demonstrates |
-| -------- | ------ | -------------------- |
-| `notebooks/01_kinopt_educational_workflow.ipynb` | `kinopt` | Kinase/phosphosite-style input tables, KinOpt preprocessing arrays, alpha/beta constraints, local optimization, a ranked multistart solution ensemble, parameter export, and fit plots. |
-| `notebooks/02_tfopt_educational_workflow.ipynb` | `tfopt` | TF-target regulatory networks, TF protein/phosphosite effect matrices, constrained local optimization, ranked multistart outputs, regulatory-effect visualization, and saved tables. |
-| `notebooks/03_protwise_educational_workflow.ipynb` | `phoskintime.protwise` | Protein-wise ODE modeling with mRNA/protein/phosphosite modalities, mode-aware fitting logic, Diffrax-based ODE solving, JAXopt parameter estimation, multistart ranking, residual plots, and CSV/JSON exports. |
+| Notebook | Module | What it demonstrates |
+|--------------------------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `notebooks/01_kinopt_educational_workflow.ipynb` | `kinopt` | Kinase/phosphosite-style input tables, KinOpt preprocessing arrays, alpha/beta constraints, local optimization, a ranked multistart solution ensemble, parameter export, and fit plots. |
+| `notebooks/02_tfopt_educational_workflow.ipynb` | `tfopt` | TF-target regulatory networks, TF protein/phosphosite effect matrices, constrained local optimization, ranked multistart outputs, regulatory-effect visualization, and saved tables. |
+| `notebooks/03_protwise_educational_workflow.ipynb` | `phoskintime.protwise` | Protein-wise ODE modeling with mRNA/protein/phosphosite modalities, mode-aware fitting logic, Diffrax-based ODE solving, JAXopt parameter estimation, multistart ranking, residual plots, and CSV/JSON exports. |
| `notebooks/04_networkmodel_educational_workflow.ipynb` | `phoskintime.networkmodel` | Network-level multimodal data handling, adjacency construction, alpha/beta projection utilities, missing-modality cases, Diffrax/JAX-based solving, local constrained optimization, ranked multistart outputs, mode exports, and network/parameter plots. |
Run the notebooks interactively:
diff --git a/common/__init__.py b/common/__init__.py
index c1495b5..33a10dc 100644
--- a/common/__init__.py
+++ b/common/__init__.py
@@ -1 +1,8 @@
-from common.utils.display import format_duration
\ No newline at end of file
+"""Shared PhosKinTime utilities."""
+
+
+def format_duration(seconds):
+ """Format a duration in seconds without importing heavy common utilities at package import time."""
+ from common.utils.display import format_duration as _format_duration
+
+ return _format_duration(seconds)
diff --git a/common/results.py b/common/results.py
new file mode 100644
index 0000000..24dd799
--- /dev/null
+++ b/common/results.py
@@ -0,0 +1,241 @@
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import platform
+import shutil
+import subprocess
+import sys
+from contextlib import contextmanager
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Iterable
+
+STANDARD_SUBDIRS = ("tables", "plots", "logs", "reports", "artifacts")
+
+
+def ensure_result_dir(outdir: str | Path) -> dict[str, Path]:
+ root = Path(outdir).expanduser().resolve()
+ root.mkdir(parents=True, exist_ok=True)
+ paths = {"root": root}
+ for name in STANDARD_SUBDIRS:
+ p = root / name
+ p.mkdir(parents=True, exist_ok=True)
+ paths[name] = p
+ return paths
+
+
+def file_sha256(path: str | Path, chunk_size: int = 1024 * 1024) -> str | None:
+ p = Path(path)
+ if not p.is_file():
+ return None
+ h = hashlib.sha256()
+ with p.open("rb") as fh:
+ for chunk in iter(lambda: fh.read(chunk_size), b""):
+ h.update(chunk)
+ return h.hexdigest()
+
+
+def describe_inputs(paths: Iterable[str | Path | None]) -> list[dict[str, Any]]:
+ out = []
+ for raw in paths:
+ if raw is None:
+ continue
+ p = Path(raw).expanduser()
+ item: dict[str, Any] = {"path": str(p)}
+ if p.exists():
+ try:
+ item["resolved_path"] = str(p.resolve())
+ except OSError:
+ pass
+ if p.is_file():
+ item["sha256"] = file_sha256(p)
+ item["size_bytes"] = p.stat().st_size
+ else:
+ item["missing"] = True
+ out.append(item)
+ return out
+
+
+def package_version() -> str | None:
+ try:
+ import importlib.metadata as metadata
+ return metadata.version("phoskintime")
+ except Exception:
+ pass
+ try:
+ import tomllib
+ with Path("pixi.toml").open("rb") as fh:
+ return tomllib.load(fh).get("workspace", {}).get("version")
+ except Exception:
+ return None
+
+
+def git_commit() -> str | None:
+ try:
+ return subprocess.check_output(
+ ["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True
+ ).strip()
+ except Exception:
+ return None
+
+
+def pixi_environment() -> str | None:
+ return os.environ.get("PIXI_ENVIRONMENT_NAME") or os.environ.get("PIXI_ENVIRONMENT")
+
+
+def command_text(argv: Iterable[str] | None = None) -> str:
+ args = list(sys.argv if argv is None else argv)
+ return " ".join(shlex_quote(a) for a in args)
+
+
+def shlex_quote(value: str) -> str:
+ import shlex
+ return shlex.quote(str(value))
+
+
+def to_json_safe(value: Any) -> Any:
+ """Convert values such as NumPy arrays/scalars and Paths to JSON-safe objects."""
+ if isinstance(value, Path):
+ return str(value)
+ if isinstance(value, dict):
+ return {str(k): to_json_safe(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple, set)):
+ return [to_json_safe(v) for v in value]
+ if hasattr(value, "tolist") and callable(value.tolist):
+ return to_json_safe(value.tolist())
+ if hasattr(value, "item") and callable(value.item):
+ try:
+ return to_json_safe(value.item())
+ except (TypeError, ValueError):
+ pass
+ if hasattr(value, "__dict__"):
+ return to_json_safe(vars(value))
+ try:
+ json.dumps(value)
+ return value
+ except TypeError:
+ return str(value)
+
+
+def _jsonable(value: Any) -> Any:
+ return to_json_safe(value)
+
+
+def write_command(outdir: str | Path, argv: Iterable[str] | None = None) -> Path:
+ root = ensure_result_dir(outdir)["root"]
+ path = root / "command.txt"
+ path.write_text(command_text(argv) + "\n", encoding="utf-8")
+ return path
+
+
+def write_metadata(
+ outdir: str | Path,
+ workflow: str,
+ args: Any | None = None,
+ inputs: Iterable[str | Path | None] = (),
+ extra: dict[str, Any] | None = None,
+) -> Path:
+ root = ensure_result_dir(outdir)["root"]
+ metadata = {
+ "workflow": workflow,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "command_arguments": _jsonable(args) if args is not None else sys.argv[1:],
+ "output_directory": str(root),
+ "package_version": package_version(),
+ "git_commit": git_commit(),
+ "python_version": platform.python_version(),
+ "python_executable": sys.executable,
+ "pixi_environment": pixi_environment(),
+ "inputs": describe_inputs(inputs),
+ }
+ if extra:
+ metadata.update(_jsonable(extra))
+ path = root / "metadata.json"
+ path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ return path
+
+
+def write_resolved_config(outdir: str | Path, config: Any | None) -> Path | None:
+ if config is None:
+ return None
+ root = ensure_result_dir(outdir)["root"]
+ path = root / "config_resolved.yaml"
+ try:
+ import yaml # type: ignore
+ text = yaml.safe_dump(_jsonable(config), sort_keys=True)
+ except Exception:
+ text = json.dumps(_jsonable(config), indent=2, sort_keys=True)
+ path.write_text(text, encoding="utf-8")
+ return path
+
+
+@contextmanager
+def tee_console_log(outdir: str | Path):
+ root = ensure_result_dir(outdir)["root"]
+ log_path = root / "console.log"
+ class Tee:
+ def __init__(self, stream, fh):
+ self.stream = stream
+ self.fh = fh
+ def write(self, data):
+ self.stream.write(data)
+ self.fh.write(data)
+ def flush(self):
+ self.stream.flush(); self.fh.flush()
+ with log_path.open("a", encoding="utf-8") as fh:
+ old_out, old_err = sys.stdout, sys.stderr
+ sys.stdout, sys.stderr = Tee(old_out, fh), Tee(old_err, fh)
+ try:
+ yield log_path
+ finally:
+ sys.stdout, sys.stderr = old_out, old_err
+
+
+def attach_file_console_logger(logger, outdir: str | Path, filename: str = "console.log"):
+ import logging
+ root = ensure_result_dir(outdir)["root"]
+ log_path = root / filename
+ abs_path = str(log_path.resolve())
+ for handler in logger.handlers:
+ if getattr(handler, "baseFilename", None) == abs_path:
+ return log_path
+ handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
+ handler.setLevel(logging.DEBUG)
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
+ logger.addHandler(handler)
+ return log_path
+
+
+def populate_standard_subdirs(outdir: str | Path, *, copy: bool = True) -> None:
+ paths = ensure_result_dir(outdir)
+ root = paths["root"]
+ skip_names = set(STANDARD_SUBDIRS) | {"metadata.json", "command.txt", "console.log", "config_resolved.yaml"}
+ table_ext = {".csv", ".tsv", ".xlsx", ".xls", ".parquet", ".json", ".npy", ".npz"}
+ plot_ext = {".png", ".jpg", ".jpeg", ".svg", ".pdf", ".html"}
+ report_names = {"report.html"}
+ artifact_ext = {".pkl", ".pickle", ".joblib"}
+ for p in list(root.iterdir()):
+ if p.name in skip_names or p.is_dir():
+ continue
+ ext = p.suffix.lower()
+ if p.name in report_names:
+ dest_dir = paths["reports"]
+ elif ext in table_ext:
+ dest_dir = paths["tables"]
+ elif ext in plot_ext:
+ dest_dir = paths["plots"]
+ elif ext in artifact_ext:
+ dest_dir = paths["artifacts"]
+ elif ext == ".log":
+ dest_dir = paths["logs"]
+ else:
+ dest_dir = paths["artifacts"]
+ dest = dest_dir / p.name
+ if dest.exists():
+ continue
+ if copy:
+ shutil.copy2(p, dest)
+ else:
+ shutil.move(str(p), str(dest))
diff --git a/config.toml b/config.toml
index e327196..039db06 100644
--- a/config.toml
+++ b/config.toml
@@ -1,115 +1,212 @@
# ============================================================
-# Global project paths and settings
+# GLOBAL PROJECT PATHS
+# ============================================================
+# Shared directory configuration used across PhosKinTime workflows.
+#
+# Update these paths for each major run mode to avoid overwriting results.
+# For example:
+#
+# python phoskintime kinopt --mode local
+#
+# can write to:
+#
+# results_dir = "results_kinopt_local"
+# logs_dir = "results_kinopt_local/logs"
+#
+# Recommended:
+# - Use one results_dir per workflow/mode/scenario.
+# - Keep logs_dir inside the matching results_dir.
+# - Keep data_dir stable unless running on a separate prepared dataset.
# ============================================================
-
-# Update these directory names for the different commmands to write data and results
-# for each mode to separate folders.
-# Otherwise, results may be overwritten when running different modes.
-# Example, for running let's say kinopt in local mode:
-# Command - python phoskintime kinopt --mode local
-# Directories can be named accordingly, e.g., results_kinopt_local
-# Updating both, results_dir and logs_dir is recommended.
[paths]
-data_dir = "data"
-results_dir = "results_model_jax"
-logs_dir = "results_model_jax/logs"
-ode_data_dir = "data"
+
+# Directory containing input data files.
+# Most workflow-specific input paths are resolved relative to this directory.
+data_dir = ""
+
+# Main output directory for result tables, plots, fitted parameters,
+# diagnostic files, and workflow metadata.
+results_dir = ""
+
+# Directory for log files.
+# Keep this inside results_dir so logs stay attached to the corresponding run.
+logs_dir = ""
+
+# Input directory used specifically by the downstream ODE/protwise pipeline.
+# Usually this is the same as data_dir unless ODE inputs are stored separately.
+ode_data_dir = ""
# ============================================================
-# TF OPTIMIZATION (mRNA → TF)
+# TF OPTIMIZATION
+# ============================================================
+# TFOpt estimates regulatory weights for the mRNA -> TF / TF -> target layer.
+#
# Used by:
# - tfopt.local.config.constants
# - tfopt.evol.config.constants
+#
+# Input paths are resolved relative to [paths].data_dir.
+# Output is written under [paths].results_dir unless overridden by the runner.
# ============================================================
[tfopt]
-# --------------------
-# Input files
-# --------------------
-input1 = "input1.csv" # expression / mRNA
-input3 = "input3.csv" # TF activity / Rout-Limma
-input4 = "input4.csv" # TF metadata / network
-
-# --------------------
-# Output
-# --------------------
-out_file = "tfopt_results.xlsx"
-
-# --------------------
-# Time grid (minutes)
-# --------------------
+
+# ------------------------------------------------------------
+# INPUT FILES
+# ------------------------------------------------------------
+
+# mRNA / expression time-course data.
+# Expected to contain gene identifiers and expression values across RNA time points.
+input1 = ""
+
+# TF activity table, for example Rout-Limma or equivalent TF activity estimates.
+# Used as the regulatory input layer for TFOpt.
+input3 = ""
+
+# TF-target network or metadata table.
+# Expected to define TF -> target relationships.
+input4 = ""
+
+
+# ------------------------------------------------------------
+# OUTPUT FILE
+# ------------------------------------------------------------
+
+# Excel output containing optimized TF alpha/beta values and diagnostics.
+out_file = ""
+
+
+# ------------------------------------------------------------
+# TIME GRID
+# ------------------------------------------------------------
+# RNA / expression time points in minutes.
+# These must match the columns or inferred sampling structure of input1.
time_points = [
4, 8, 15, 30, 60,
120, 240, 480, 960
]
-# --------------------
-# Default optimization parameters
-# (used unless overridden by CLI or mode)
-# --------------------
+
+# ------------------------------------------------------------
+# DEFAULT OPTIMIZATION SETTINGS
+# ------------------------------------------------------------
+# Default parameter bounds used unless overridden by a mode-specific section.
+#
+# Bounds define the feasible range for TF regulatory effects.
+# Negative values allow repression; positive values allow activation.
lower_bound = -4.0
upper_bound = 4.0
-# Loss types (integer, matches tfopt code)
-# 0: MSE
-# 1: MAE
-# 2: soft L1
-# 3: Cauchy
-# 4: Arctan
-# 5: Elastic Net
-# 6: Tikhonov
+# Loss function used by TFOpt.
+#
+# Integer codes must match the TFOpt implementation:
+#
+# 0 = MSE
+# 1 = MAE
+# 2 = Soft L1
+# 3 = Cauchy
+# 4 = Arctan
+# 5 = Elastic Net
+# 6 = Tikhonov
+#
+# Recommended:
+# Use robust or regularized losses for noisy transcriptomics data.
loss_type = 5
-# --------------------
-# Mode-specific overrides
-# --------------------
+# ------------------------------------------------------------
+# LOCAL MODE OVERRIDES
+# ------------------------------------------------------------
+# Settings used when running:
+#
+# python phoskintime tfopt --mode local
+#
+# Intended for deterministic local optimization / refinement.
[tfopt.modes.local]
-# SLSQP / TRUST-CONSTR defaults
-# (keep wide bounds for local refinement)
+
+# Local optimizer parameter bounds.
+# Keep these consistent with global defaults unless a narrower local search
+# region is intentionally required.
lower_bound = -4.0
upper_bound = 4.0
-loss_type = 5
+# Loss function used in local TFOpt mode.
+loss_type = 5
+
+
+# ------------------------------------------------------------
+# EVOLUTIONARY MODE OVERRIDES
+# ------------------------------------------------------------
+# Legacy/global evolutionary TFOpt settings.
+#
+# Use only if the evolutionary backend is still supported in the current codebase.
+# If TFOpt has been migrated to JAX/JAXopt-only local optimization, this section
+# should be retained only for backward compatibility or removed entirely.
[tfopt.modes.evol]
-# Global evolutionary search defaults
+
+# Evolutionary search parameter bounds.
lower_bound = -4.0
upper_bound = 4.0
-loss_type = 5
-# Optimizer choices:
-# 0: NSGA-II
-# 1: SMSEMOA
-# 2: AGEMOEA
+# Loss function used in evolutionary TFOpt mode.
+loss_type = 5
+
+# Evolutionary optimizer backend.
+#
+# Integer codes must match the TFOpt implementation:
+#
+# 0 = NSGA-II
+# 1 = SMSEMOA
+# 2 = AGEMOEA
optimizer = 0
# ============================================================
-# KINASE OPTIMIZATION (Kinase → Phosphosite)
+# KINASE OPTIMIZATION
+# ============================================================
+# KinOpt estimates kinase -> phosphosite regulatory weights.
+#
# Used by:
# - kinopt.local.config.constants
# - kinopt.evol.config.constants
+#
+# Input paths are resolved relative to [paths].data_dir.
+# Output is written under [paths].results_dir unless overridden by the runner.
# ============================================================
[kinopt]
-# --------------------
-# Input files (relative to data_dir)
-# --------------------
-input1 = "input1.csv" # kinase / protein abundance
-input2 = "input2.csv" # phosphosite data
-# --------------------
-# Output
-# --------------------
-out_file = "kinopt_results.xlsx"
+# ------------------------------------------------------------
+# INPUT FILES
+# ------------------------------------------------------------
+
+# Protein / kinase abundance time-course data.
+# Typically contains protein-level measurements used as kinase activity proxies.
+input1 = ""
+
+# Phosphosite input table or kinase-substrate network input.
+# Expected to contain phosphosite-level measurements and/or kinase-site mappings,
+# depending on the KinOpt implementation.
+input2 = ""
-# --------------------
-# Time grid (minutes)
-# --------------------
+
+# ------------------------------------------------------------
+# OUTPUT FILE
+# ------------------------------------------------------------
+
+# Excel output containing optimized kinase alpha/beta values and diagnostics.
+out_file = ""
+
+
+# ------------------------------------------------------------
+# TIME GRID
+# ------------------------------------------------------------
+# Protein/phosphoproteomics time points in minutes.
+# These must match the MS time-course structure of the input files.
time_points = [
0.0, 0.5, 0.75, 1.0,
2.0, 4.0, 8.0, 16.0,
@@ -117,133 +214,369 @@ time_points = [
240.0, 480.0, 960.0
]
-# --------------------
-# Default optimization parameters
-# --------------------
+
+# ------------------------------------------------------------
+# DEFAULT OPTIMIZATION SETTINGS
+# ------------------------------------------------------------
+
+# Default bounds for kinase-phosphosite regulatory effects.
+# Negative values allow inhibitory effects; positive values allow activating effects.
lower_bound = -4.0
upper_bound = 4.0
-# Loss types (string, matches kinopt code)
-# base | weighted | softl1 | cauchy | arctan
+# KinOpt loss function.
+#
+# String values must match the KinOpt implementation:
+#
+# "base" = unweighted base loss
+# "weighted" = time- or data-weighted loss
+# "softl1" = robust Soft L1 loss
+# "cauchy" = robust Cauchy loss
+# "arctan" = robust Arctan loss
loss_type = "weighted"
-# Estimate missing kinase–psite pairs?
+# Estimate missing kinase-phosphosite relationships when direct evidence is absent.
+#
+# true:
+# Infer missing kinase-site effects where allowed by the model.
+#
+# false:
+# Use only observed/provided kinase-site relationships.
estimate_missing_kinases = true
-# Scaling options:
-# min_max | log | temporal | segmented | slope | cumulative | none
+# Scaling method for kinase/protein/phosphosite time-course values.
+#
+# Options:
+# "min_max" = scale values to a min-max range
+# "log" = log-transform values
+# "temporal" = apply temporal scaling around split_point
+# "segmented" = scale by predefined time segments
+# "slope" = emphasize trajectory slopes
+# "cumulative" = emphasize cumulative signal
+# "none" = no scaling
scaling_method = "none"
-# Used for temporal scaling
+# Split index used by temporal scaling.
+# Only relevant when scaling_method = "temporal".
split_point = 9
-# Used for segmented scaling
+# Segment boundaries used by segmented scaling.
+# Only relevant when scaling_method = "segmented".
segment_points = [0, 3, 6, 9, 14]
-# Optimization method (local)
-# slsqp | trust-constr
+# Local optimization method.
+#
+# Options:
+# "slsqp" = constrained gradient-based local optimization
+# "trust-constr" = trust-region constrained optimization
+#
+# Use only if the current KinOpt backend still supports these SciPy methods.
+# If KinOpt has been migrated to JAXopt, this field should be mapped to the
+# corresponding JAXopt solver or kept only for backward compatibility.
method = "slsqp"
-# --------------------
-# Mode-specific overrides
-# --------------------
-
+# ------------------------------------------------------------
+# LOCAL MODE OVERRIDES
+# ------------------------------------------------------------
+# Settings used when running:
+#
+# python phoskintime kinopt --mode local
+#
+# Intended for deterministic local optimization.
[kinopt.modes.local]
-# Trust the defaults; override if needed
+
+# Local optimizer method.
+# Keep consistent with the default method unless testing an alternative backend.
method = "slsqp"
+
+# ------------------------------------------------------------
+# EVOLUTIONARY MODE OVERRIDES
+# ------------------------------------------------------------
+# Legacy/global evolutionary KinOpt settings.
+#
+# Use only if the evolutionary backend is still supported.
+# If KinOpt is now JAX/JAXopt-only, this section should be retained only for
+# backward compatibility or removed.
[kinopt.modes.evol]
-# Narrower bounds for global search
+
+# Evolutionary search bounds.
lower_bound = -4.0
upper_bound = 4.0
-# Loss choices for evol
-# base | autocorrelation | huber | mape | weighted
+# Loss function used in evolutionary KinOpt mode.
+#
+# Supported values depend on the evol implementation:
+#
+# "base"
+# "autocorrelation"
+# "huber"
+# "mape"
+# "weighted"
loss_type = "base"
-# Regularization (used only in evol)
+# Apply regularization during evolutionary optimization.
+#
+# false:
+# Fit only the selected loss.
+#
+# true:
+# Add regularization penalty if implemented by the backend.
regularization = false
-# Evolutionary optimizer backend
-# DE | NSGA-II (string)
+# Evolutionary optimizer backend.
+#
+# Supported values depend on implementation:
+# "DE"
+# "NSGA-II"
method = "NSGA-II"
# ============================================================
-# ODE PIPELINE (downstream of kinopt + tfopt)
-# Used by:
-# - config.constants
+# PROTWISE ODE PIPELINE CONFIGURATION
+# ============================================================
+# Configuration block used by protwise.runner.main.
+#
+# This pipeline runs the downstream ODE model after KinOpt and TFOpt outputs
+# are available. It fits protein, RNA, and phosphosite dynamics using the
+# configured model type, parameter bounds, time grids, and input files.
# ============================================================
[ode]
-# Which ODE model
-# distmod | succmod | randmod
-
-# After setting the results_dir and logs_dir globally above,
-# you can set the model type here to have separate output folders
-# for each model type.
+# ODE model variant to run.
+#
+# Supported options:
+# distmod = distributive phosphorylation model
+# succmod = successive / sequential phosphorylation model
+# randmod = random / non-ordered phosphorylation model
+#
+# The selected model can be used by the runner to create model-specific
+# output folders and filenames.
model = "distmod"
-# Upper bounds used in parameter fitting / priors
+
+# ------------------------------------------------------------
+# PARAMETER BOUNDS
+# ------------------------------------------------------------
+# Upper bounds for fitted ODE parameters.
+#
+# These bounds constrain the search space during parameter estimation.
+# Keep them broad enough to allow fitting, but not so broad that the
+# optimizer can produce biologically implausible rates.
[ode.bounds]
-mRNA_prod = 20
-mRNA_deg = 20
+
+# Maximum mRNA production rate.
+mRNA_prod = 20
+
+# Maximum mRNA degradation rate.
+mRNA_deg = 20
+
+# Maximum protein production / translation rate.
protein_prod = 20
-protein_deg = 20
+
+# Maximum protein degradation / turnover rate.
+protein_deg = 20
+
+# Maximum phosphorylation production / activation rate.
phospho_prod = 20
-phospho_deg = 20
-# Bootstrap settings
+# Maximum phosphosite dephosphorylation / decay rate.
+phospho_deg = 20
+
+
+# ------------------------------------------------------------
+# BOOTSTRAP SETTINGS
+# ------------------------------------------------------------
+# Number of bootstrap replicates used for uncertainty estimation.
+#
+# Higher values improve uncertainty estimates but increase runtime.
+# Use small values for testing and larger values for final analyses.
[ode.bootstrap]
-n = 0
+n = 10
-# Time grids (minutes)
+
+# ------------------------------------------------------------
+# TIME GRIDS
+# ------------------------------------------------------------
+# Experimental sampling times in minutes.
+#
+# These must match the time points present in the input data after
+# preprocessing. Protein and phosphosite measurements usually share the
+# MS time grid. RNA can have a different time grid.
[ode.time]
+
+# Protein / proteomics time points.
protein = [0.0, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
-rna = [4.0, 8.0, 15.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
-# Fit controls
+# RNA / transcriptomics time points.
+rna = [4.0, 8.0, 15.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
+
+
+# ------------------------------------------------------------
+# FIT CONTROLS
+# ------------------------------------------------------------
+# Controls how model predictions are compared against observed data.
[ode.fit]
+
+# Normalize model predictions before computing the fitting loss.
+#
+# false:
+# Compare model output directly to processed input values.
+#
+# true:
+# Normalize predictions before scoring. Use only when the data and
+# model outputs are intended to be compared on a normalized scale.
normalize_model_output = false
-use_custom_weights = false
-use_regularization = true
+# Use custom time-point or modality weights during fitting.
+#
+# false:
+# Use default/uniform weighting.
+#
+# true:
+# Use custom weights if implemented by the protwise fitting routine.
+use_custom_weights = false
+
+# Apply regularization during parameter fitting.
+#
+# true:
+# Penalize unstable or implausible parameter solutions.
+#
+# false:
+# Fit only to data without regularization penalty.
+use_regularization = true
+
+
+# ------------------------------------------------------------
+# COMPOSITE LOSS WEIGHTS
+# ------------------------------------------------------------
+# Weights for different error summaries used in the composite objective.
+#
+# Increase a weight to make that error metric contribute more strongly.
+# Set a weight to 0.0 to disable that component.
[ode.fit.composite_weights]
+
+# Root mean squared error weight.
rmse = 1.0
-mae = 1.0
-var = 1.0
-mse = 1.0
-l2 = 1.0
-# Sensitivity
+# Mean absolute error weight.
+mae = 1.0
+
+# Variance-based error weight.
+var = 1.0
+
+# Mean squared error weight.
+mse = 1.0
+
+# L2 norm error weight.
+l2 = 1.0
+
+
+# ------------------------------------------------------------
+# SENSITIVITY ANALYSIS
+# ------------------------------------------------------------
+# Optional post-fit sensitivity analysis.
[ode.sensitivity]
-enabled = true
+
+# Enable or disable sensitivity analysis.
+#
+# false:
+# Skip sensitivity analysis.
+#
+# true:
+# Run sensitivity analysis after fitting.
+enabled = false
+
+# Relative parameter perturbation used for sensitivity calculations.
+#
+# Example:
+# 0.5 = 50% perturbation.
+#
+# Use smaller values for local sensitivity and larger values for broader
+# robustness checks.
perturbation = 0.5
+
+# ------------------------------------------------------------
+# MORRIS SENSITIVITY SETTINGS
+# ------------------------------------------------------------
+# Settings for Morris-style global sensitivity analysis.
[ode.sensitivity.morris]
+
+# Number of Morris trajectories.
+#
+# Higher values improve sampling coverage but increase runtime.
num_trajectories = 1000
-num_levels = 400
-# Plot
+# Number of grid levels used for parameter perturbation.
+#
+# Higher values provide finer perturbation resolution but increase cost.
+num_levels = 400
+
+
+# ------------------------------------------------------------
+# PLOTTING SETTINGS
+# ------------------------------------------------------------
+# Controls visualization of sensitivity or perturbation traces.
[ode.plot]
+
+# Opacity for individual perturbed trajectories in plots.
+#
+# Low values reduce overplotting when many trajectories are drawn.
perturb_trace_opacity = 0.02
-# Inputs to the ODE pipeline.
-# Make these explicit and stable (no importing from other modules).
+
+# ------------------------------------------------------------
+# INPUT FILES
+# ------------------------------------------------------------
+# Input paths consumed by the protwise ODE pipeline.
+#
+# Paths are relative to the project root unless the runner resolves them
+# relative to another configured base directory.
+#
+# Keep these paths explicit. Do not rely on imports from unrelated modules.
[ode.inputs]
-# These are paths relative to PROJECT ROOT. You can also make them relative to data_dir if you prefer.
-protein_excel = "data/input1.csv"
-psite_excel = "data/kinopt_results.xlsx"
-rna_excel = "data/tfopt_results.xlsx"
-# Output naming (optional)
+# Protein / proteomics input table.
+#
+# This should contain protein-level time-course observations used by the
+# ODE fitting routine.
+protein_excel = ""
+
+# KinOpt output file.
+#
+# Expected to contain phosphosite-level kinase-substrate optimization results,
+# priors, or interaction weights used by the ODE model.
+psite_excel = ""
+
+# TFOpt output file.
+#
+# Expected to contain TF-target optimization results, priors, or regulatory
+# weights used by the ODE model.
+rna_excel = ""
+
+
+# ------------------------------------------------------------
+# OUTPUT FILE NAMING
+# ------------------------------------------------------------
+# Optional output directory and result file names.
+#
+# Leave empty only if protwise.runner.main has fallback naming logic,
+# such as "_results" and "_results.xlsx".
[ode.output]
-out_dir_name = "results_protwise_jax" # if empty, code can use "_results"
-out_xlsx_name = "results_protwise_jax.xlsx" # if empty, code can use "_results.xlsx"
+
+# Directory name for this protwise run.
+#
+# Use model- and scenario-specific names to avoid overwriting previous runs.
+out_dir_name = ""
+
+# Main Excel output filename for fitted parameters, predictions,
+# diagnostics, and summary tables.
+out_xlsx_name = ""
# ============================================================
-# GLOBAL MODEL (Integrated ODE Optimization)
+# NETWORK MODEL
# Used by: networkmodel.runner
# ============================================================
@@ -251,151 +584,589 @@ out_xlsx_name = "results_protwise_jax.xlsx" # if empty, code can use " protein-level MS observations
+# - Rows with non-empty Psite -> phosphosite-level MS observations
+# - Empty string "" -> modality intentionally absent
+#
+# Therefore:
+# - A mixed MS file can contain both protein rows and phospho rows.
+# - A protein-only file should contain only rows where Psite is empty.
+# - A phospho-only file should contain only rows where Psite is non-empty.
+# - RNA can be left as "" if transcriptomics data are not available.
+#
+# Current implementation note:
+# In the current load_data() shown earlier, `phospho` is not read separately.
+# Phospho data are extracted from `ms` by checking non-empty Psite values.
+# Keep `phospho = ""` unless you add explicit separate phospho-file loading.
+# ------------------------------------------------------------
+
+# Kinase-substrate network.
+# Required.
+# Expected to contain columns such as GeneID/protein/gene, Psite/site, and Kinase/K.
+# Used to build the kinase -> phosphosite mechanistic topology.
+kinase_net = ""
+
+# TF-gene regulatory network.
+# Required if transcriptional regulation is part of the model.
+# Expected to contain source/TF and target/gene columns.
+# If no TF network is available, use a valid empty TF network file with headers,
+# not an empty string, unless load_data() is explicitly modified to allow missing TF input.
+tf_net = ""
+
+# Mass-spec input table.
+# This is the main MS observation file consumed by load_data().
+#
+# Scenario A: mixed protein + phospho MS file
+# ms = "data/input1.csv"
+# - protein rows: Psite empty
+# - phospho rows: Psite non-empty
+#
+# Scenario B: protein-only MS file
+# ms = "data/input1_protein_only.csv"
+# - all retained rows have empty Psite
+# - df_prot is populated
+# - df_pho is empty
+#
+# Scenario C: phospho-only MS file
+# ms = "data/input1_phospho_only.csv"
+# - all retained rows have non-empty Psite
+# - df_pho is populated
+# - df_prot is empty
+#
+# Do not label a phospho-only file as protein MS data.
+ms = ""
+
+# RNA / transcriptomics input table.
+#
+# Scenario A: RNA available
+# rna = "data/rna.csv"
+# - file should contain gene/mRNA/protein identifiers and time-series columns
+# - contributes to RNA loss if lambda_rna > 0
+#
+# Scenario B: RNA unavailable
+# rna = ""
+# - load_data() should create an empty dataframe with columns: protein, time, fc
+# - recommended: set regularization_rna = 0.0 for this run
+#
+# Requires the missing-RNA guard in load_data(); otherwise pandas will try to read "" as a filename.
+rna = ""
+
+# Separate phospho input table.
+#
+# Current behavior:
+# This option is currently unused by the shown load_data().
+# Phospho observations are taken from `ms` using non-empty Psite rows.
+#
+# Recommended current setting:
+# phospho = ""
+#
+# Future behavior, only if separate loading is implemented:
+# phospho = "data/input1_phospho_only.csv"
+# ms = "data/input1_protein_only.csv"
+#
+# Do not set this expecting it to work unless load_data() explicitly reads args.phospho.
+phospho = ""
+
+# ------------------------------------------------------------
+# PRIOR OPTIMIZATION RESULTS
# ------------------------------------------------------------
-kinase_net = "data/input2.csv" # Kinase-Substrate Network (or similar)
-tf_net = "data/input4.csv" # TF-Gene Network
-ms = "data/input1.csv" # Mass Spec (Protein) Data
-rna = "data/input3.csv" # RNA (Transcriptomics) Data
-phospho = "data/input1.csv" # Phospho Data (Optional, can be same as ms if mixed)
+# Optional Excel files from previous KinOpt / TFOpt runs.
+# These are used as fixed prior weights if available:
+#
+# kinopt:
+# - Alpha Values: kinase-substrate interaction strengths
+# - Beta Values : kinase-level or kinase-site priors
+#
+# tfopt:
+# - Alpha Values: TF-target interaction strengths
+# - Beta Values : TF-level priors
+#
+# Leave as "" if no prior optimization results are available.
+# In that case, the model should fall back to default weights, usually 1.0.
+kinopt = ""
+tfopt = ""
-# Previous optimization results (Priors)
-kinopt = "data/kinopt_results.xlsx"
-tfopt = "data/tfopt_results.xlsx"
# ------------------------------------------------------------
# RUN SETTINGS
# ------------------------------------------------------------
-output_dir = "results_model_global_distributive_jax" # Output directory
-cores = 80 # 0 or leave out to use all available cores
-seed = 42
+# Directory where all outputs, logs, plots, fitted parameters,
+# diagnostics, and dashboard files will be written.
+#
+# Use a scenario-specific name to avoid overwriting previous runs.
+output_dir = ""
+
+# Number of CPU cores/workers used for parallel parts of the workflow.
+#
+# Recommended:
+# - Use a positive integer for controlled runs.
+# - Avoid using all system cores if JAX/XLA, BLAS, or multiprocessing
+# are also active, because oversubscription can slow the run down.
+#
+# Example:
+# cores = 8 # conservative local run
+# cores = 32 # HPC / large server run
+# cores = 80 # high-core node, only if available
+cores = 80
+
+# Random seed used for reproducible initialization, multistart sampling,
+# posterior sampling, and other stochastic components where applicable.
+seed = 42
+
+
+# ------------------------------------------------------------
+# OPTIMIZATION SETTINGS
+# ------------------------------------------------------------
+# Maximum number of local optimizer iterations.
+#
+# Historical name:
+# n_gen
+#
+# Current meaning:
+# maximum JAXopt local-optimization iterations
+#
+# This is not an evolutionary generation count anymore.
+n_gen = 100
+
+
# ------------------------------------------------------------
-# OPTIMIZATION PARAMETERS (JAXopt)
+# LOSS FUNCTION
# ------------------------------------------------------------
-n_gen = 30 # Maximum local optimizer iterations (runner arg: --n-gen)
+# Loss function used to compare predicted and observed trajectories.
+#
+# Available options:
+#
+# -1 = Charbonnier loss
+# 0 = Mean squared error (MSE)
+# 1 = Huber loss
+# 2 = Pseudo-Huber loss
+# 3 = Log-cosh loss
+# 4 = Cauchy loss
+# 5 = Poisson loss
+# 6 = Geman-McClure loss
+#
+# Recommended defaults:
+#
+# loss = 0
+# Use when data are clean and you want strong penalty for large residuals.
+#
+# loss = 1 or 2
+# Use for noisy omics data where outliers should not dominate the fit.
+# Pseudo-Huber is smooth and usually safer for JAX-based optimization.
+#
+# loss = 3
+# Smooth robust alternative; often stable for continuous fold-change data.
+#
+# loss = 4 or 6
+# Strongly robust losses; useful when outliers are severe, but they may
+# downweight large residuals aggressively.
+#
+# loss = 5
+# Use only for count-like non-negative data. Usually not appropriate for
+# scaled log-fold-change proteomics/transcriptomics data.
loss = 2
-# Loss type (0: MSE, 1:Huber, 2:Psuedo-Huber, 3:Log-Cosh, 4:Cauchy, 5:Poisson, 6:Geman-McClure)
-# -1 for Charbonnier Loss
# ------------------------------------------------------------
-# LOSS WEIGHTS (Regularization)
+# LOSS WEIGHTS
# ------------------------------------------------------------
-lambda_prior = 0.1 # Weight for prior adherence (runner arg: --lambda-prior)
-lambda_protein = 1.0 # Weight for protein error (runner arg: --lambda-protein)
-lambda_rna = 1.0 # Weight for RNA error (runner arg: --lambda-rna)
-lambda_phospho = 1.0 # Weight for Phospho error (runner arg: --lambda-phospho)
+# These weights control how strongly each loss component contributes
+# to the final scalar objective optimized by JAXopt.
+#
+# General rule:
+# - Set weight > 0 when the modality is present and should guide fitting.
+# - Set weight = 0.0 when the modality is absent or should be ignored.
+#
+# Scenario examples:
+# Protein-only run:
+# lambda_protein = 1.0
+# lambda_rna = 0.0
+# lambda_phospho = 0.0
+#
+# Phospho-only run:
+# lambda_protein = 0.0
+# lambda_rna = 0.0
+# lambda_phospho = 1.0
+#
+# Protein + phospho run:
+# lambda_protein = 1.0
+# lambda_rna = 0.0
+# lambda_phospho = 1.0
+#
+# Full multi-modal run:
+# lambda_protein = 1.0
+# lambda_rna = 1.0
+# lambda_phospho = 1.0
+#
+# lambda_prior penalizes deviation from prior/default parameter values.
+# Increase it for stronger regularization; decrease it for more data-driven fitting.
+lambda_prior = 0.1
+lambda_protein = 1.0
+lambda_rna = 1.0
+lambda_phospho = 1.0
+
# ------------------------------------------------------------
-# HYPERPARAMETERS SCAN OPTIONS
+# HYPERPARAMETER SCAN
# ------------------------------------------------------------
-hyperparam_scan = false # Enable hyperparameter scan
+# Enable automatic scanning of loss weights / regularization settings.
+#
+# false:
+# Use the manually specified lambda_* values above.
+#
+# true:
+# Run the hyperparameter scan before final optimization.
+# This is slower and should be used only when scanning logic is active
+# and maintained in the current JAXopt workflow.
+hyperparam_scan = false
+
# ------------------------------------------------------------
# DATA INFERENCE FLAGS
# ------------------------------------------------------------
-normalize_fc_steady = false # Normalize data to t=0 (runner arg: --normalize-fc-steady)
-use_initial_condition_from_data = true # Use data for t=0 state (runner arg: --use-initial-condition-from-data)
+# Normalize fold-change trajectories relative to the baseline time point.
+#
+# false:
+# Use the values produced by the selected scaling_method.
+#
+# true:
+# Re-normalize protein/phospho trajectories to t = 0 after loading.
+# Use only when baseline-relative comparison is required.
+normalize_fc_steady = false
+
+# Use observed baseline data to initialize model states.
+#
+# true:
+# Initial conditions are inferred from available t=0 / baseline observations.
+#
+# false:
+# Initial conditions use model defaults.
+#
+# Recommended:
+# true when baseline observations are reliable.
+# false when baseline data are missing, sparse, or noisy.
+use_initial_condition_from_data = true
+
# ------------------------------------------------------------
-# DATA SCALING OPTIONS
+# DATA SCALING
# ------------------------------------------------------------
-# - 'raw' / 'none': No scaling. Returns raw intensities/counts in tidy format.
-# - 'fc_start': Standard Fold-Change (x_t / x_0).
-# - 'robust_fc': Fold-Change with noise of floor value (x_t / (x_0 + eps)).
-# - 'max_scale': Normalizes to [0, 1] range (x_t / x_max).
-# - 'mean_scale': Centers data around 1.0 (x_t / x_mean).
-# - 'l2_norm': Unit vector scaling (x_t / ||x||).
-scaling_method = "raw" # Data scaling method
+# Scaling method applied during input preprocessing.
+#
+# Options:
+#
+# "raw" / "none"
+# No scaling. Use raw intensities/counts as provided.
+# Suitable only if input values are already normalized and comparable.
+#
+# "fc_start"
+# Standard fold-change relative to the first time point:
+# x_t / x_0
+# Useful for time-course proteomics/phosphoproteomics.
+#
+# "robust_fc"
+# Fold-change with a small denominator floor:
+# x_t / (x_0 + eps)
+# Safer when baseline values can be near zero.
+#
+# "max_scale"
+# Scale each trajectory by its maximum:
+# x_t / x_max
+# Produces values roughly in [0, 1].
+#
+# "mean_scale"
+# Scale each trajectory by its mean:
+# x_t / x_mean
+# Centers each trajectory around 1.0.
+#
+# "l2_norm"
+# Normalize each trajectory by its vector norm:
+# x_t / ||x||
+# Useful when trajectory shape matters more than absolute scale.
+#
+# Recommended:
+# - Use "raw" only if preprocessing was already done externally.
+# - Use "fc_start" or "robust_fc" for most fold-change time-course runs.
+scaling_method = "raw"
+
# ------------------------------------------------------------
-# DATA WEIGHTING OPTIONS
+# DATA WEIGHTING
# ------------------------------------------------------------
+# Time-point weighting schemes for each modality.
+#
+# These control whether early, late, baseline-proximal, or central time points
+# contribute more strongly to the loss.
+#
# Options:
-# 'uniform', 'linear_early', 'linear_late', 'exp_early',
-# 'exp_late', 'quad_early', 'quad_late', 'log_early', 'inv_time',
-# 'inv_sqrt_time', 'piecewise_early_boost', 'gaussian_center', 'logistic_center',
-# 'distance_from_baseline'
+# "uniform"
+# All time points receive equal weight.
+#
+# "linear_early", "exp_early", "quad_early", "log_early"
+# Earlier time points receive more weight.
+#
+# "linear_late", "exp_late", "quad_late"
+# Later time points receive more weight.
+#
+# "inv_time", "inv_sqrt_time"
+# Earlier time points receive stronger inverse-time weighting.
+#
+# "piecewise_early_boost"
+# Early phase receives a fixed boost.
+#
+# "gaussian_center", "logistic_center"
+# Middle/transition-region time points receive more weight.
+#
+# "distance_from_baseline"
+# Weights depend on distance from baseline.
+#
+# Recommended default:
+# "uniform"
+#
+# Note:
+# In the runner code shown earlier, protein and RNA weighting are passed
+# into build_weight_functions(). Confirm that phospho weighting is also
+# wired into the objective before relying on weighting_method_phospho.
+weighting_method_protein = "uniform"
+weighting_method_rna = "uniform"
+weighting_method_phospho = "uniform"
-weighting_method_protein = "uniform" # Weighting for protein data
-weighting_method_rna = "uniform" # Weighting for RNA data
-weighting_method_phospho = "uniform" # Weighting for phospho data
# ------------------------------------------------------------
# SENSITIVITY ANALYSIS
# ------------------------------------------------------------
-sensitivity_analysis = false # Enable sensitivity analysis (runner arg: --sensitivity-analysis)
-sensitivity_perturbation = 0.05 # Perturbation for sensitivity analysis
-sensitivity_trajectories = 100 # Number of trajectories for Morris method
-sensitivity_levels = 40 # Number of levels for Morris method
-sensitivity_top_curves = 20 # Number of top curves to plot in sensitivity analysis
-sensitivity_metric = "l2_norm" # ["total_signal", "mean", "variance", "l2_norm"]
-# ------------------------------------------------------------
+# Enable post-optimization sensitivity analysis.
+#
+# false:
+# Skip sensitivity analysis.
+#
+# true:
+# Run sensitivity analysis after fitting the model.
+sensitivity_analysis = false
+
+# Relative perturbation size used for local sensitivity calculations.
+# Example:
+# 0.05 = ±5% perturbation.
+sensitivity_perturbation = 0.05
+
+# Number of sampled trajectories for Morris/global sensitivity analysis.
+# Higher values improve coverage but increase runtime.
+sensitivity_trajectories = 100
+
+# Number of discretization levels for Morris sensitivity sampling.
+# Higher values give finer parameter perturbation resolution.
+sensitivity_levels = 40
+
+# Number of highest-sensitivity curves to include in plots/reports.
+sensitivity_top_curves = 20
+
+# Metric used to summarize trajectory sensitivity.
+#
+# Options:
+# "total_signal" = integrated total response
+# "mean" = mean trajectory value
+# "variance" = trajectory variability
+# "l2_norm" = Euclidean trajectory magnitude
+sensitivity_metric = "l2_norm"
+
# ------------------------------------------------------------
# INFERENCE / POST-OPTIMIZATION ANALYSES
# ------------------------------------------------------------
-# --- Inference / Multistart ---
-n_starts = 1 # int: restarts; 1 = single-start (current behaviour)
-# --- Profile Likelihood (post-optimization) ---
+
+# Number of optimization restarts.
+#
+# 1:
+# Single-start local optimization.
+#
+# >1:
+# Multistart local optimization. Runs multiple initializations and keeps
+# the best solution according to the scalar objective.
+#
+# Recommended:
+# Use n_starts > 1 for non-convex ODE models.
+n_starts = 1
+
+
+# ------------------------------------------------------------
+# PROFILE LIKELIHOOD
+# ------------------------------------------------------------
+# Enable profile likelihood after optimization.
+#
+# false:
+# Skip profile likelihood.
+#
+# true:
+# Profile selected parameters listed in profile_indices.
profile_likelihood = false
-profile_indices = "88" # , 93, 95, 103, 108, 69, 78, 80, 116, 125, 135, 136, 139" # comma-separated parameter indices e.g. "0,1,5"
+
+# Comma-separated parameter indices to profile.
+#
+# Example:
+# profile_indices = "0,1,5,10"
+#
+# Leave empty to disable profiling even if profile_likelihood is true.
+profile_indices = ""
+
+# Number of grid points per profiled parameter.
+# Higher values give smoother profiles but increase runtime.
profile_grid_size = 5
-# --- Posterior Sampling (post-optimization, requires numpyro) ---
+
+
+# ------------------------------------------------------------
+# POSTERIOR SAMPLING
+# ------------------------------------------------------------
+# Enable posterior sampling after optimization.
+#
+# Requires NumPyro and a working posterior objective.
+# This is useful for uncertainty quantification, but is substantially slower
+# than deterministic optimization.
posterior_sampling = false
+
+# Number of warmup/adaptation samples for posterior sampling.
posterior_num_warmup = 20
+
+# Number of retained posterior samples.
posterior_num_samples = 30
-# MODEL & BOUNDS CONFIGURATION
-# ------------------------------------------------------------
+# ------------------------------------------------------------
+# MODEL TIME POINTS
+# ------------------------------------------------------------
+# Time grids used by the model and loss mapping.
+#
+# These must match the experimental sampling times after preprocessing.
+# Units must be consistent across protein, phospho, and RNA data.
+#
+# protein:
+# Time points for protein-level MS observations.
+#
+# phospho_protein:
+# Time points for phosphosite-level observations.
+#
+# rna:
+# Time points for RNA/transcriptomics observations.
+#
+# If a modality is absent, its time grid can remain defined, but the
+# corresponding input table should be empty and its lambda should usually be 0.0.
[networkmodel.timepoints]
protein = [0.0, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
phospho_protein = [0.0, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
rna = [4.0, 8.0, 15.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]
+
+# ------------------------------------------------------------
+# PARAMETER BOUNDS
+# ------------------------------------------------------------
+# Bounds define the feasible range for each optimized parameter group.
+#
+# Keep bounds biologically plausible but not so tight that the optimizer
+# cannot fit real dynamics.
+#
+# Format:
+# parameter = [lower_bound, upper_bound]
[networkmodel.bounds]
-# Kinase Activity Multiplier: Keep as is, 4.0 is a reasonable max fold-change
+
+# Kinase activity multiplier.
+# Scales kinase input/activity relative to the prior/default.
+#
+# Recommended:
+# Lower bound > 0 to avoid inactive or numerically unstable kinase drivers.
+# Upper bound around 4.0 allows strong but not extreme activation.
c_k = [1e-3, 4.0]
-# Basal mRNA Transcription: Keep wide to allow for low-abundance genes
+# Basal transcription rate.
+# Controls mRNA production.
+#
+# Wide range allows both low- and high-expression genes.
A_i = [1e-6, 10.0]
-# mRNA Degradation: 0.1 is ~7min half-life, 0.001 is ~11 hours.
+# mRNA degradation rate.
+# Controls RNA decay.
+#
+# Approximate interpretation:
+# 1e-3 = slow degradation
+# 1.0 = fast degradation
B_i = [1e-3, 1.0]
-# Translation Rate (The "Balloon" Variable):
-# Lower the max significantly. Proteins rarely translate at 100x basal speed.
+# Translation rate.
+# Controls protein production from mRNA.
+#
+# Keep the upper bound moderate to avoid unrealistic protein blow-up.
C_i = [1e-3, 2.0]
-# Protein Deactivation (State reset):
-# Increase the floor. 1e-6 is essentially "never turns off."
+# Protein degradation/deactivation rate.
+# Controls decay/reset of protein-level state.
+#
+# Avoid extremely small lower bounds if long-term simulations show runaway
+# or non-decaying protein trajectories.
D_i = [0.1, 0.5]
-# Phospho-site Dephosphorylation:
-# Usually faster than total protein deactivation.
+# Phosphosite dephosphorylation rate.
+# Usually faster than total protein degradation/deactivation.
Dp_i = [0.05, 5.0]
-# Transcriptional Efficacy (Alpha):
-# 10.0 is a strong regulator.
+# Initial condition / state scaling parameter.
+# Allows protein-specific adjustment of initial state magnitude.
+#
+# Use a positive lower bound to avoid zero-state collapse.
E_i = [1e-4, 10.0]
-# TF Scaling (The "Sensitivity" exponent):
-# If using Exponential coupling, +/- 4.0 is very high (e^4 = 54x).
-# +/- 2.0 (e^2 = 7x) is often more stable for global models.
+# Global TF scaling parameter.
+# Controls the strength of transcriptional regulatory coupling.
+#
+# Current bounds allow positive amplification only.
+# If signed TF scaling is intended, use symmetric bounds such as [-2.0, 2.0]
+# and confirm the model equations support negative values.
tf_scale = [2.0, 10.0]
+
+# ------------------------------------------------------------
+# MODEL SELECTION
+# ------------------------------------------------------------
+# Available mechanistic phosphorylation models.
+#
+# default_model selects which model is used for the current run.
+#
+# Options:
+# "distributive"
+# "sequential"
+# "combinatorial"
+# "saturation"
[networkmodel.models]
available_models = ["distributive", "sequential", "combinatorial", "saturation"]
default_model = "distributive"
+
+# ------------------------------------------------------------
+# ODE SOLVER SETTINGS
+# ------------------------------------------------------------
+# Numerical tolerances for ODE integration.
+#
+# Lower tolerance values increase accuracy but also increase runtime.
+# For stiff biological ODE systems, tight tolerances can be expensive.
+#
+# Recommended starting point:
+# absolute_tolerance = 1e-8
+# relative_tolerance = 1e-8
+#
+# If simulations are slow or fail frequently, relax to 1e-6.
[networkmodel.solver]
absolute_tolerance = 1e-8
relative_tolerance = 1e-8
-max_timesteps = 200000
\ No newline at end of file
+
+# Maximum number of solver internal time steps.
+# Increase this if the solver stops early due to step-limit errors.
+# Decrease only if runaway trajectories should fail faster.
+max_timesteps = 200000
diff --git a/config/cli.py b/config/cli.py
index b35687f..fcd2f46 100644
--- a/config/cli.py
+++ b/config/cli.py
@@ -54,7 +54,7 @@ def _run(cmd: list[str]) -> None:
sp.check_call([PY, "-m", *cmd], cwd=ROOT)
-def _python_module(module: str, cfg: Path | None) -> list[str]:
+def _python_module(module: str, cfg: Path | None, outdir: Path | None = None) -> list[str]:
"""
Return `python module [--conf path]`.
@@ -67,6 +67,8 @@ def _python_module(module: str, cfg: Path | None) -> list[str]:
cmd = [module]
if cfg is not None:
cmd += ["--conf", str(cfg)]
+ if outdir is not None:
+ cmd += ["--outdir", str(outdir)]
return cmd
@@ -84,6 +86,7 @@ def prep():
@app.command()
def tfopt(
mode: str = typer.Option("local", help="local | evol"),
+ outdir: Path | None = typer.Option(None, "--outdir", "--output-dir", file_okay=False, dir_okay=True, help="Directory for workflow outputs."),
conf: Path | None = typer.Option(
None, "--conf", file_okay=True, dir_okay=False, writable=False,
help="Path to TOML/YAML config. Uses defaults if omitted."
@@ -99,12 +102,13 @@ def tfopt(
None
"""
module = f"tfopt.{mode}"
- _run(_python_module(module, conf))
+ _run(_python_module(module, conf, outdir if mode == "local" else None))
@app.command()
def kinopt(
mode: str = typer.Option("local", help="local | evol"),
+ outdir: Path | None = typer.Option(None, "--outdir", "--output-dir", file_okay=False, dir_okay=True, help="Directory for workflow outputs."),
conf: Path | None = typer.Option(
None, "--conf", file_okay=True, dir_okay=False, writable=False,
help="Path to TOML/YAML config. Uses defaults if omitted."
@@ -120,11 +124,12 @@ def kinopt(
None
"""
module = f"kinopt.{mode}"
- _run(_python_module(module, conf))
+ _run(_python_module(module, conf, outdir if mode == "local" else None))
@app.command()
def model(
+ outdir: Path | None = typer.Option(None, "--outdir", "--output-dir", file_okay=False, dir_okay=True, help="Directory for workflow outputs."),
conf: Path | None = typer.Option(
None, "--conf", file_okay=True, dir_okay=False, writable=False,
help="Path to model config file. Uses defaults if omitted."
@@ -138,11 +143,12 @@ def model(
Returns:
None
"""
- _run(_python_module("protwise.runner.main", conf))
+ _run(_python_module("protwise.runner.main", conf, outdir))
@app.command()
def networkmodel(
+ outdir: Path | None = typer.Option(None, "--outdir", "--output-dir", file_okay=False, dir_okay=True, help="Directory for workflow outputs."),
conf: Path | None = typer.Option(
"config.toml", "--conf", file_okay=True, dir_okay=False, writable=False,
help="Path to global model config file. Uses config.toml by default."
@@ -155,7 +161,7 @@ def networkmodel(
section of your configuration.
"""
# Assuming the package is named 'networkmodel' and has a 'runner.py'
- _run(_python_module("networkmodel.runner", conf))
+ _run(_python_module("networkmodel.runner", conf, outdir))
@app.command()
@@ -201,6 +207,7 @@ def all(
tf_conf: Path | None = typer.Option(None, help="tfopt config file"),
kin_conf: Path | None = typer.Option(None, help="kinopt config file"),
model_conf: Path | None = typer.Option(None, help="model config file"),
+ outdir: Path | None = typer.Option(None, "--outdir", "--output-dir", file_okay=False, dir_okay=True, help="Base directory for workflow outputs."),
):
"""
Run every stage in sequence.
@@ -220,9 +227,9 @@ def all(
None
"""
prep()
- tfopt.callback(mode=tf_mode, conf=tf_conf)
- kinopt.callback(mode=kin_mode, conf=kin_conf)
- model.callback(conf=model_conf)
+ tfopt.callback(mode=tf_mode, conf=tf_conf, outdir=(outdir / "tfopt" if outdir else None))
+ kinopt.callback(mode=kin_mode, conf=kin_conf, outdir=(outdir / "kinopt" if outdir else None))
+ model.callback(conf=model_conf, outdir=(outdir / "protwise" if outdir else None))
if __name__ == "__main__":
diff --git a/config/config.py b/config/config.py
index 4434f3f..c6951ef 100644
--- a/config/config.py
+++ b/config/config.py
@@ -2,7 +2,12 @@
import argparse
import numpy as np
from pathlib import Path
-from typing import Optional
+from typing import Optional, Any
+
+try:
+ import tomllib
+except ModuleNotFoundError: # pragma: no cover
+ import tomli as tomllib
from numba import njit
from config.constants import (
@@ -82,44 +87,156 @@ def ensure_output_directory(directory):
os.makedirs(directory, exist_ok=True)
-def parse_args():
- """
- Parse command-line arguments for the PhosKinTime script.
- This function uses argparse to define and handle the command-line options.
- It includes options for setting bounds, fixed parameters, bootstrapping,
- profile estimation, and input file paths.
- The function returns the parsed arguments as a Namespace object.
- The arguments include:
- --A-bound, --B-bound, --C-bound, --D-bound,
- --Ssite-bound, --Dsite-bound, --bootstraps,
- --input-excel-protein, --input-excel-psite, --input-excel-rna.
- Args:
- None
- Returns:
- argparse.Namespace: The parsed command-line arguments.
- """
+def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
+ out = dict(base)
+ for key, value in (override or {}).items():
+ if isinstance(value, dict) and isinstance(out.get(key), dict):
+ out[key] = _deep_merge(out[key], value)
+ else:
+ out[key] = value
+ return out
+
+
+def default_config_path() -> Path:
+ return PROJECT_ROOT / "config.toml"
+
+
+def parse_config_path(argv: list[str] | None = None) -> Path | None:
+ pre_parser = argparse.ArgumentParser(add_help=False)
+ pre_parser.add_argument("--conf", default=None)
+ known, _ = pre_parser.parse_known_args(argv)
+ return Path(known.conf).expanduser() if known.conf else None
+
+
+def _model_type(model: str) -> str:
+ return {
+ "protwise": "Protein-wise",
+ "distmod": "Distributive",
+ "succmod": "Successive",
+ "randmod": "Random",
+ }.get(str(model), "Unknown")
+
+
+def load_selected_config(conf_path: str | Path | None = None) -> dict[str, Any]:
+ selected = Path(conf_path).expanduser().resolve() if conf_path else default_config_path().resolve()
+ with selected.open("rb") as handle:
+ raw = tomllib.load(handle)
+ ode = raw.get("ode", {}) or {}
+ modes = ode.get("modes", {}) or {}
+ merged = _deep_merge(ode, modes.get("local", {}) or {})
+ merged["_paths"] = raw.get("paths", {}) or {}
+ merged["_root"] = str(PROJECT_ROOT)
+ merged["_config_path"] = str(selected)
+ merged["_config_source"] = "custom" if conf_path else "default"
+ return merged
+
+
+def _path_from_config(root: Path, value: str | Path) -> Path:
+ path = Path(value).expanduser()
+ return path if path.is_absolute() else root / path
+
+
+def _defaults_from_loaded_config(loaded_config: dict[str, Any]) -> dict[str, Any]:
+ root = Path(loaded_config.get("_root", PROJECT_ROOT))
+ paths = loaded_config.get("_paths", {}) or {}
+ bounds = loaded_config.get("bounds", {}) or {}
+ bootstrap = loaded_config.get("bootstrap", {}) or {}
+ time = loaded_config.get("time", {}) or {}
+ inputs = loaded_config.get("inputs", {}) or {}
+ output = loaded_config.get("output", {}) or {}
+ fit = loaded_config.get("fit", {}) or {}
+ weights = fit.get("composite_weights", {}) or {}
+ sensitivity = loaded_config.get("sensitivity", {}) or {}
+ morris = sensitivity.get("morris", {}) or {}
+
+ model = str(loaded_config.get("model", "randmod"))
+ model_type = _model_type(model)
+ results_dir = _path_from_config(root, paths.get("results_dir", "results"))
+ out_dir_name = str(output.get("out_dir_name") or "").strip()
+ out_xlsx_name = str(output.get("out_xlsx_name") or "").strip()
+ out_dir = results_dir / (out_dir_name or f"{model_type}_results")
+
+ return {
+ "conf": None if loaded_config.get("_config_source") == "default" else loaded_config.get("_config_path"),
+ "resolved_config_path": loaded_config.get("_config_path"),
+ "config_source": loaded_config.get("_config_source", "default"),
+ "A_bound": (0.0, float(bounds.get("mRNA_prod", 20))),
+ "B_bound": (0.0, float(bounds.get("mRNA_deg", 20))),
+ "C_bound": (0.0, float(bounds.get("protein_prod", 20))),
+ "D_bound": (0.0, float(bounds.get("protein_deg", 20))),
+ "Ssite_bound": (0.0, float(bounds.get("phospho_prod", 20))),
+ "Dsite_bound": (0.0, float(bounds.get("phospho_deg", bounds.get("protein_deg", 20)))),
+ "bootstraps": int(bootstrap.get("n", 0)),
+ "input_excel_protein": _path_from_config(root, inputs["protein_excel"]) if inputs.get("protein_excel") else "",
+ "input_excel_psite": _path_from_config(root, inputs["psite_excel"]) if inputs.get("psite_excel") else "",
+ "input_excel_rna": _path_from_config(root, inputs["rna_excel"]) if inputs.get("rna_excel") else "",
+ "outdir": out_dir,
+ "out_results_dir": out_dir / (out_xlsx_name or f"{model_type}_results.xlsx"),
+ "time_points": np.asarray(time.get("protein", [0.0, 0.5, 0.75, 1.0, 2.0, 4.0, 8.0, 16.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]), dtype=float),
+ "time_points_rna": np.asarray(time.get("rna", [4.0, 8.0, 15.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0]), dtype=float),
+ "model": model,
+ "model_type": model_type,
+ "dev_test": bool(loaded_config.get("dev_test", False)),
+ "use_regularization": bool(fit.get("use_regularization", True)),
+ "alpha_ci": float(loaded_config.get("alpha_ci", 0.95)),
+ "weights": {
+ "alpha": float(weights.get("rmse", 1.0)),
+ "beta": float(weights.get("mae", 1.0)),
+ "gamma": float(weights.get("var", 1.0)),
+ "delta": float(weights.get("mse", 1.0)),
+ "mu": float(weights.get("l2", 1.0)),
+ },
+ "sensitivity": {
+ "enabled": bool(sensitivity.get("enabled", True)),
+ "metric": str(loaded_config.get("y_metric", "total_signal")),
+ "num_trajectories": int(morris.get("num_trajectories", 1000)),
+ "num_levels": int(morris.get("num_levels", 400)),
+ "perturbation": float(sensitivity.get("perturbation", 0.5)),
+ },
+ }
+
+
+def build_parser(defaults: dict[str, Any]) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="PhosKinTime - ODE Parameter Estimation of Cell Signalling Events in Temporal Space"
)
- parser.add_argument("--A-bound", type=parse_bound_pair, default=f"0, {UB_mRNA_prod}")
- parser.add_argument("--B-bound", type=parse_bound_pair, default=f"0, {UB_mRNA_deg}")
- parser.add_argument("--C-bound", type=parse_bound_pair, default=f"0, {UB_Protein_prod}")
- parser.add_argument("--D-bound", type=parse_bound_pair, default=f"0, {UB_Protein_deg}")
- parser.add_argument("--Ssite-bound", type=parse_bound_pair, default=f"0, {UB_Phospho_prod}")
- parser.add_argument("--Dsite-bound", type=parse_bound_pair, default=f"0, {UB_Protein_deg}")
- parser.add_argument("--bootstraps", type=int, default=BOOTSTRAPS)
- parser.add_argument("--input-excel-protein", type=str,
- default=INPUT_EXCEL_PROTEIN,
- help="Path to the original protein data file")
- parser.add_argument("--input-excel-psite", type=str,
- default=INPUT_EXCEL_PSITE,
- help="Path to the estimated optimized phosphorylation-residue file")
- parser.add_argument("--input-excel-rna", type=str,
- default=INPUT_EXCEL_RNA,
- help="Path to the estimated optimized mRNA-TF file")
- return parser.parse_args()
-
+ parser.add_argument("--conf", default=defaults["conf"], help="Path to ProtWise/ODE TOML config file.")
+ parser.add_argument("--A-bound", type=parse_bound_pair, default=defaults["A_bound"])
+ parser.add_argument("--B-bound", type=parse_bound_pair, default=defaults["B_bound"])
+ parser.add_argument("--C-bound", type=parse_bound_pair, default=defaults["C_bound"])
+ parser.add_argument("--D-bound", type=parse_bound_pair, default=defaults["D_bound"])
+ parser.add_argument("--Ssite-bound", type=parse_bound_pair, default=defaults["Ssite_bound"])
+ parser.add_argument("--Dsite-bound", type=parse_bound_pair, default=defaults["Dsite_bound"])
+ parser.add_argument("--bootstraps", type=int, default=defaults["bootstraps"])
+ parser.add_argument("--input-excel-protein", type=str, default=str(defaults["input_excel_protein"]), help="Path to the original protein data file")
+ parser.add_argument("--input-excel-psite", type=str, default=str(defaults["input_excel_psite"]), help="Path to the estimated optimized phosphorylation-residue file")
+ parser.add_argument("--input-excel-rna", type=str, default=str(defaults["input_excel_rna"]), help="Path to the estimated optimized mRNA-TF file")
+ parser.add_argument("--outdir", "--output-dir", dest="outdir", type=str, default=str(defaults["outdir"]), help="Directory where all run outputs and provenance files are written.")
+ return parser
+
+
+def parse_args(argv: list[str] | None = None):
+ selected_config = parse_config_path(argv)
+ if selected_config is not None:
+ os.environ["PHOSKINTIME_ODE_CONFIG"] = str(selected_config.expanduser().resolve())
+ loaded_config = load_selected_config(selected_config)
+ defaults = _defaults_from_loaded_config(loaded_config)
+ parser = build_parser(defaults)
+ args = parser.parse_args(argv)
+ args.resolved_config_path = str(Path(defaults["resolved_config_path"]).resolve())
+ args.config_source = defaults["config_source"]
+ args.time_points = defaults["time_points"]
+ args.time_points_rna = defaults["time_points_rna"]
+ args.out_results_dir = str(Path(args.outdir) / Path(defaults["out_results_dir"]).name)
+ args.model = defaults["model"]
+ args.model_type = defaults["model_type"]
+ args.dev_test = defaults["dev_test"]
+ args.use_regularization = defaults["use_regularization"]
+ args.alpha_ci = defaults["alpha_ci"]
+ args.weights = defaults["weights"]
+ args.sensitivity = defaults["sensitivity"]
+ return args
def log_config(logger, bounds, args):
"""
@@ -143,17 +260,11 @@ def log_config(logger, bounds, args):
np.set_printoptions(suppress=True)
-def extract_config(args):
- """
- Extract configuration settings from command-line arguments.
- This function creates a dictionary containing the parameter bounds, bootstrapping iterations.
- The function returns the configuration dictionary.
-
- Args:
- args (argparse.Namespace): The command-line arguments.
- Returns:
- dict: The configuration settings.
+def extract_config(args, loaded_config: dict[str, Any] | None = None):
+ """
+ Extract effective ProtWise runtime settings after config and CLI precedence
+ have been resolved. CLI values override values loaded from --conf/default config.
"""
bounds = {
"A": args.A_bound,
@@ -161,15 +272,29 @@ def extract_config(args):
"C": args.C_bound,
"D": args.D_bound,
"S(i)": args.Ssite_bound,
- "D(i)": args.Dsite_bound
+ "D(i)": args.Dsite_bound,
}
config = {
- 'bounds': bounds,
- 'bootstraps': args.bootstraps,
- 'input_excel_protein': args.input_excel_protein,
- 'input_excel_psite': args.input_excel_psite,
- 'input_excel_rna': args.input_excel_rna,
- 'max_workers': 1 if DEV_TEST else os.cpu_count(),
+ "bounds": bounds,
+ "bootstraps": args.bootstraps,
+ "input_excel_protein": args.input_excel_protein,
+ "input_excel_psite": args.input_excel_psite,
+ "input_excel_rna": args.input_excel_rna,
+ "max_workers": 1 if getattr(args, "dev_test", DEV_TEST) else os.cpu_count(),
+ "outdir": args.outdir,
+ "out_results_dir": args.out_results_dir,
+ "time_points": args.time_points,
+ "time_points_rna": args.time_points_rna,
+ "supplied_config_path": args.conf,
+ "resolved_config_path": args.resolved_config_path,
+ "config_source": args.config_source,
+ "model": args.model,
+ "model_type": args.model_type,
+ "dev_test": args.dev_test,
+ "use_regularization": args.use_regularization,
+ "alpha_ci": args.alpha_ci,
+ "weights": args.weights,
+ "sensitivity": args.sensitivity,
}
return config
diff --git a/config/constants.py b/config/constants.py
index ba130c1..bd9917a 100644
--- a/config/constants.py
+++ b/config/constants.py
@@ -1,3 +1,4 @@
+import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
@@ -7,12 +8,32 @@
from config.helpers import *
from config_loader import load
+try:
+ import tomllib
+except ModuleNotFoundError: # pragma: no cover
+ import tomli as tomllib
+
# -------------------------------------------------------------------------------------------------
-# Load configuration (ODE section) from config.toml
+# Load configuration (ODE section) from config.toml or the runner-selected custom file.
# This module must not import tfopt/kinopt constants to avoid hard coupling / circular dependencies.
# -------------------------------------------------------------------------------------------------
-_CFG = load("local", "ode") # mode is optional for ODE, but keeps a consistent API
-_ROOT = Path(_CFG["_root"])
+CONFIG_ENV_VAR = "PHOSKINTIME_ODE_CONFIG"
+_CONFIG_PATH = Path(os.environ[CONFIG_ENV_VAR]).expanduser().resolve() if os.environ.get(CONFIG_ENV_VAR) else None
+
+if _CONFIG_PATH is None:
+ _CFG = load("local", "ode") # mode is optional for ODE, but keeps a consistent API
+ _ROOT = Path(_CFG["_root"])
+else:
+ with _CONFIG_PATH.open("rb") as _fh:
+ _RAW_CFG = tomllib.load(_fh)
+ _ODE_BASE = (_RAW_CFG.get("ode", {}) or {})
+ _ODE_MODES = (_ODE_BASE.get("modes", {}) or {})
+ _CFG = {**_ODE_BASE, **(_ODE_MODES.get("local", {}) or {})}
+ _PATHS = _RAW_CFG.get("paths", {}) or {}
+ _ROOT = Path(__file__).resolve().parents[1]
+ _CFG["_paths"] = _PATHS
+ _CFG["_root"] = str(_ROOT)
+
_PATHS = _CFG.get("_paths", {}) or {}
# Flag to indicate if the code is in development mode.
@@ -125,17 +146,53 @@
_inputs = _CFG.get("inputs", {}) or {}
-def _req_path(key: str) -> Path:
+def _optional_path(key: str) -> Path | str:
+ """Return a configured ODE input path, or "" when it is intentionally absent.
+
+ Importing this module is part of many non-ODE code paths (logging, network model
+ tests, pytest collection). Required ODE input validation therefore happens in
+ ``validate_ode_inputs`` at workflow execution time rather than at import time.
+ """
v = _inputs.get(key)
if not v:
- raise KeyError(f"[ode.inputs] is missing required key '{key}' in config.toml")
+ return ""
return _ROOT / str(v)
-# These MUST be explicit in [ode.inputs] (no tfopt imports)
-INPUT_EXCEL_PROTEIN = _req_path("protein_excel")
-INPUT_EXCEL_PSITE = _req_path("psite_excel")
-INPUT_EXCEL_RNA = _req_path("rna_excel")
+def _validate_path_value(key: str, value, require_existing: bool) -> Path:
+ if not value:
+ raise KeyError(f"[ode.inputs] is missing required key '{key}' in config.toml")
+ path = Path(value).expanduser()
+ if require_existing and not path.exists():
+ raise ValueError(f"[ode.inputs] path for '{key}' does not exist: {path}")
+ return path
+
+
+def validate_ode_inputs(config: dict | None = None, require_existing: bool = False) -> dict[str, Path]:
+ """Validate required ODE input paths for an actual ProtWise/ODE run."""
+ if config is None:
+ values = {
+ "protein_excel": INPUT_EXCEL_PROTEIN,
+ "psite_excel": INPUT_EXCEL_PSITE,
+ "rna_excel": INPUT_EXCEL_RNA,
+ }
+ else:
+ values = {
+ "protein_excel": config.get("input_excel_protein"),
+ "psite_excel": config.get("input_excel_psite"),
+ "rna_excel": config.get("input_excel_rna"),
+ }
+ return {
+ key: _validate_path_value(key, value, require_existing)
+ for key, value in values.items()
+ }
+
+
+# These are intentionally default-safe for generic imports. Call
+# validate_ode_inputs() before executing workflows that consume them.
+INPUT_EXCEL_PROTEIN = _optional_path("protein_excel")
+INPUT_EXCEL_PSITE = _optional_path("psite_excel")
+INPUT_EXCEL_RNA = _optional_path("rna_excel")
# Ensure dirs exist
OUT_DIR.mkdir(parents=True, exist_ok=True)
diff --git a/config/logconf.py b/config/logconf.py
index d38f8f5..61938c3 100644
--- a/config/logconf.py
+++ b/config/logconf.py
@@ -112,7 +112,7 @@ def setup_logger(
- "per_process": file logging in each process
:return: logger
"""
- if not os.path.exists(log_dir):
+ if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
if name is None:
@@ -141,7 +141,7 @@ def setup_logger(
is_worker = False
# Decide file logging policy
- enable_file = True
+ enable_file = bool(log_dir)
if mp_file_logging == "off":
enable_file = False
elif mp_file_logging == "main_only" and is_worker:
diff --git a/dashboard/__init__.py b/dashboard/__init__.py
new file mode 100644
index 0000000..d8ce9db
--- /dev/null
+++ b/dashboard/__init__.py
@@ -0,0 +1 @@
+"""Dashboard result-browser package for PhosKinTime."""
diff --git a/dashboard/app.py b/dashboard/app.py
new file mode 100644
index 0000000..d81748e
--- /dev/null
+++ b/dashboard/app.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.command_builder import build_workflow_command, sanitize_run_name
+from dashboard.components.command_preview import render_command_preview
+from dashboard.components.config_panel import render_config_panel
+from dashboard.components.console_panel import render_cancellation_note, render_console
+from dashboard.components.input_preview import render_input_preview
+from dashboard.components.preset_panel import render_preset_panel
+from dashboard.components.result_browser import render_result_browser
+from dashboard.components.run_status import render_run_status
+from dashboard.components.upload_panel import render_upload_panel
+from dashboard.components.validation_panel import render_validation_panel, validate_dashboard_setup
+from dashboard.components.workflow_selector import render_workflow_selector
+from dashboard.components.workflow_tabs import render_workflow_tabs
+from dashboard.config_utils import DashboardSelection
+from dashboard.result_parser import discover_result_directory
+from dashboard.runner import log_tail, run_built_command
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+
+
+def _candidate_result_dirs(base: Path) -> list[Path]:
+ if not base.is_dir():
+ return []
+ candidates = [base]
+ candidates.extend(path for path in sorted(base.iterdir()) if path.is_dir())
+ return candidates
+
+
+def _render_browser_panel(default_directory: Path | None = None) -> None:
+ import streamlit as st
+
+ with st.sidebar:
+ st.header("Result directory")
+ default_base = default_directory.parent if default_directory else Path("results")
+ base = Path(st.text_input("Base results folder", value=str(default_base), key="browser-base")).expanduser()
+ candidates = _candidate_result_dirs(base)
+ if default_directory and default_directory.is_dir() and default_directory not in candidates:
+ candidates.insert(0, default_directory)
+ if candidates:
+ index = candidates.index(default_directory) if default_directory in candidates else 0
+ choice = st.selectbox("Select folder", candidates, index=index, format_func=lambda path: str(path),
+ key="browser-choice")
+ directory_text = st.text_input("Selected result directory", value=str(choice), key="browser-directory")
+ else:
+ st.info("No selectable folders found under the base path. Enter a result directory manually.")
+ directory_text = st.text_input("Selected result directory", value=str(base), key="browser-directory-manual")
+
+ try:
+ inventory = discover_result_directory(directory_text)
+ except (FileNotFoundError, NotADirectoryError) as exc:
+ st.error(str(exc))
+ return
+
+ if not inventory.has_content:
+ st.warning("This directory exists, but no standard PhosKinTime result files were discovered.")
+ render_result_browser(
+ inventory,
+ key_prefix=f"browser-{inventory.root.name}",
+ )
+ render_workflow_tabs(inventory.root)
+
+
+def _render_launcher_panel() -> None:
+ import streamlit as st
+
+ st.header("Workflow launcher")
+ st.write("Construct, preview, and run registered PhosKinTime workflows using existing CLI modules.")
+ render_cancellation_note()
+
+ workflow, env, run_name = render_workflow_selector(REPO_ROOT)
+ safe_run_name = sanitize_run_name(run_name)
+ uploaded_paths = render_upload_panel(REPO_ROOT, safe_run_name)
+ retained_paths = [Path(path) for path in st.session_state.get("uploaded_paths", []) if Path(path).exists()]
+ combined_paths = sorted({*retained_paths, *uploaded_paths}, key=lambda path: path.name.lower())
+ if uploaded_paths:
+ st.session_state["uploaded_paths"] = [str(path) for path in combined_paths]
+ render_input_preview(combined_paths)
+ argument_values, input_assignments = render_config_panel(workflow, combined_paths)
+ validation_problems = validate_dashboard_setup(workflow, combined_paths, input_assignments)
+ can_run = render_validation_panel(validation_problems)
+
+ try:
+ built = build_workflow_command(
+ workflow.key,
+ repo_root=REPO_ROOT,
+ pixi_environment=env,
+ run_name=safe_run_name,
+ argument_values=argument_values,
+ input_assignments=input_assignments,
+ )
+ except (KeyError, ValueError) as exc:
+ st.error(str(exc))
+ return
+
+ render_command_preview(built)
+ selection = DashboardSelection(
+ workflow_key=workflow.key,
+ run_name=safe_run_name,
+ pixi_environment=env,
+ arguments=argument_values,
+ input_assignments=input_assignments,
+ )
+ render_preset_panel(selection, REPO_ROOT)
+ render_run_status(st.session_state.get("launcher_status"), st.session_state.get("launcher_returncode"))
+
+ if st.button("Run workflow", type="primary", disabled=not can_run):
+ console_lines: list[str] = []
+ console_placeholder = st.empty()
+ status_placeholder = st.empty()
+ st.session_state["launcher_status"] = "running"
+ st.session_state["launcher_returncode"] = None
+ with status_placeholder.container():
+ render_run_status("running")
+ final_event = None
+ try:
+ for event in run_built_command(built, repo_root=REPO_ROOT):
+ final_event = event
+ if event.line:
+ console_lines.append(event.line)
+ with console_placeholder.container():
+ render_console(console_lines)
+ except FileNotFoundError as exc:
+ st.session_state["launcher_status"] = "failure"
+ st.session_state["launcher_returncode"] = 127
+ st.error(f"Could not start workflow command: {exc}")
+ return
+
+ if final_event is not None:
+ st.session_state["launcher_status"] = final_event.status
+ st.session_state["launcher_returncode"] = final_event.returncode
+ st.session_state["last_run_dir"] = str(built.outdir)
+ with status_placeholder.container():
+ render_run_status(final_event.status, final_event.returncode)
+ if final_event.status == "failure":
+ st.subheader("Log tail")
+ st.code(log_tail(built.outdir), language="text")
+ elif final_event.status == "success":
+ st.success("Run completed. The result directory is shown below.")
+ try:
+ inventory = discover_result_directory(built.outdir)
+ render_result_browser(
+ inventory,
+ key_prefix=f"launcher-{workflow.key}-{safe_run_name}",
+ )
+ render_workflow_tabs(inventory.root)
+ except (FileNotFoundError, NotADirectoryError) as exc:
+ st.warning(f"Run finished, but the result directory could not be opened: {exc}")
+
+
+def main() -> None:
+ import streamlit as st
+
+ st.set_page_config(page_title="PhosKinTime Dashboard", layout="wide")
+ st.title("PhosKinTime Dashboard")
+ st.write(
+ "Browse existing result directories or launch registered CLI workflows without reimplementing scientific logic.")
+
+ launcher_tab, browser_tab = st.tabs(["Run workflow", "Browse results"])
+ with launcher_tab:
+ _render_launcher_panel()
+ with browser_tab:
+ last_run_dir = st.session_state.get("last_run_dir")
+ _render_browser_panel(Path(last_run_dir) if last_run_dir else None)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dashboard/command_builder.py b/dashboard/command_builder.py
new file mode 100644
index 0000000..0a0144d
--- /dev/null
+++ b/dashboard/command_builder.py
@@ -0,0 +1,135 @@
+from __future__ import annotations
+
+import re
+import shlex
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+from dashboard.registry import ArgumentSpec, WorkflowDescriptor, get_workflow
+
+_SAFE_RUN_CHARS = re.compile(r"[^A-Za-z0-9._-]+")
+
+
+@dataclass(frozen=True)
+class BuiltCommand:
+ """A safely constructed workflow command."""
+
+ workflow: WorkflowDescriptor
+ command: list[str]
+ outdir: Path
+ pixi_environment: str
+
+ @property
+ def preview(self) -> str:
+ return " ".join(shlex.quote(str(part)) for part in self.command)
+
+
+def sanitize_run_name(name: str) -> str:
+ """Return a filesystem-safe run name from dashboard input."""
+ cleaned = _SAFE_RUN_CHARS.sub("-", name.strip()).strip(".-_")
+ return cleaned or "run"
+
+
+def build_output_dir(repo_root: str | Path, workflow_key: str, run_name: str,
+ output_base: str | Path = "results") -> Path:
+ """Build a project-local output directory for a workflow run."""
+ root = Path(repo_root).resolve()
+ base = Path(output_base).expanduser()
+ if not base.is_absolute():
+ base = root / base
+ safe_name = sanitize_run_name(run_name)
+ return (base / workflow_key / safe_name).resolve()
+
+
+def _coerce_argument(spec: ArgumentSpec, value: Any) -> list[str]:
+ if value is None or value == "":
+ if spec.required:
+ raise ValueError(f"Missing required argument: {spec.name}")
+ return []
+ if spec.kind == "bool":
+ enabled = value
+ if isinstance(value, str):
+ enabled = value.strip().lower() in {"1", "true", "yes", "y", "on"}
+ return [spec.flag] if bool(enabled) else []
+ if spec.kind == "int":
+ return [spec.flag, str(int(value))]
+ if spec.kind == "float":
+ return [spec.flag, str(float(value))]
+ return [spec.flag, str(value)]
+
+
+def workflow_arguments(workflow: WorkflowDescriptor, values: dict[str, Any] | None = None) -> list[str]:
+ """Convert structured argument values into a safe argv fragment."""
+ values = values or {}
+ accepted = {spec.name: spec for spec in workflow.accepted_arguments}
+ unknown = sorted(set(values) - set(accepted))
+ if unknown:
+ raise ValueError(f"Unsupported arguments for {workflow.key}: {', '.join(unknown)}")
+
+ args: list[str] = []
+ for spec in workflow.accepted_arguments:
+ value = values.get(spec.name, spec.default)
+ args.extend(_coerce_argument(spec, value))
+ return args
+
+
+def arguments_from_input_assignments(workflow: WorkflowDescriptor,
+ input_assignments: dict[str, str | Path] | None = None) -> dict[str, str]:
+ """Map workflow input roles to supported CLI argument names."""
+ input_assignments = input_assignments or {}
+ specs = {spec.role: spec for spec in workflow.input_assignments}
+ unknown = sorted(set(input_assignments) - set(specs))
+ if unknown:
+ raise ValueError(f"Unsupported input roles for {workflow.key}: {', '.join(unknown)}")
+
+ mapped: dict[str, str] = {}
+ for role, path in input_assignments.items():
+ if path is None or str(path) == "":
+ continue
+ spec = specs[role]
+ if spec.argument_name is None:
+ continue
+ mapped[spec.argument_name] = str(path)
+ return mapped
+
+
+def merge_argument_sources(
+ workflow: WorkflowDescriptor,
+ argument_values: dict[str, Any] | None = None,
+ input_assignments: dict[str, str | Path] | None = None,
+) -> dict[str, Any]:
+ """Merge structured parameters with input assignments without inventing CLI options."""
+ merged = dict(argument_values or {})
+ for name, value in arguments_from_input_assignments(workflow, input_assignments).items():
+ merged[name] = value
+ return merged
+
+
+def build_workflow_command(
+ workflow_key: str,
+ *,
+ repo_root: str | Path = ".",
+ pixi_environment: str = "default",
+ run_name: str = "run",
+ output_base: str | Path = "results",
+ argument_values: dict[str, Any] | None = None,
+ input_assignments: dict[str, str | Path] | None = None,
+ use_pixi: bool = True,
+) -> BuiltCommand:
+ """Build a workflow command as argv list without shell interpolation."""
+ workflow = get_workflow(workflow_key)
+ if not workflow.python_module:
+ raise ValueError(f"Workflow {workflow_key!r} does not define a Python module command.")
+
+ outdir = build_output_dir(repo_root, workflow.key, run_name, output_base)
+ command: list[str] = []
+ if use_pixi:
+ command.extend(["pixi", "run", "-e", pixi_environment])
+ command.extend(["python", "-m", workflow.python_module])
+ command.extend(workflow.module_args)
+ merged_arguments = merge_argument_sources(workflow, argument_values, input_assignments)
+ command.extend(workflow_arguments(workflow, merged_arguments))
+ if workflow.output_dir_arg:
+ command.extend([workflow.output_dir_arg, str(outdir)])
+ return BuiltCommand(workflow=workflow, command=command, outdir=outdir, pixi_environment=pixi_environment)
diff --git a/dashboard/components/__init__.py b/dashboard/components/__init__.py
new file mode 100644
index 0000000..a23e77a
--- /dev/null
+++ b/dashboard/components/__init__.py
@@ -0,0 +1 @@
+"""Streamlit components for the PhosKinTime result browser."""
diff --git a/dashboard/components/analysis_panel.py b/dashboard/components/analysis_panel.py
new file mode 100644
index 0000000..fb04fc0
--- /dev/null
+++ b/dashboard/components/analysis_panel.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.workflow_panels.analysis import ANALYSIS_TASKS, build_analysis_command, discover_analysis_outputs
+
+
+def render_analysis_panel(result_dir: str | Path) -> None:
+ """Compatibility wrapper for rendering advanced analysis controls."""
+ from dashboard.workflow_panels.analysis import render
+
+ render(result_dir)
+
+
+__all__ = ["ANALYSIS_TASKS", "build_analysis_command", "discover_analysis_outputs", "render_analysis_panel"]
diff --git a/dashboard/components/command_preview.py b/dashboard/components/command_preview.py
new file mode 100644
index 0000000..aff903b
--- /dev/null
+++ b/dashboard/components/command_preview.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from dashboard.command_builder import BuiltCommand
+
+
+def render_command_preview(built: BuiltCommand) -> None:
+ """Show an exact command preview before execution."""
+ import streamlit as st
+
+ st.subheader("Command preview")
+ st.code(built.preview, language="bash")
+ with st.expander("Argument list", expanded=False):
+ st.json(built.command)
+ st.caption(f"Output directory: `{built.outdir}`")
diff --git a/dashboard/components/config_panel.py b/dashboard/components/config_panel.py
new file mode 100644
index 0000000..dd14b29
--- /dev/null
+++ b/dashboard/components/config_panel.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from dashboard.config_utils import parse_config_file
+from dashboard.registry import WorkflowDescriptor
+
+
+def render_config_panel(workflow: WorkflowDescriptor, available_files: list[Path]) -> tuple[
+ dict[str, Any], dict[str, str]]:
+ import streamlit as st
+
+ """Render structured parameter and workflow-specific input assignment controls."""
+ st.subheader("Configure workflow")
+ arguments: dict[str, Any] = {}
+ assignments: dict[str, str] = {}
+
+ if workflow.accepted_arguments:
+ with st.expander("Parameters", expanded=True):
+ for spec in workflow.accepted_arguments:
+ if any(input_spec.argument_name == spec.name for input_spec in workflow.input_assignments):
+ continue
+ help_text = spec.description or None
+ default = "" if spec.default is None else str(spec.default)
+ if spec.kind == "bool":
+ arguments[spec.name] = st.checkbox(spec.flag, value=bool(spec.default), help=help_text)
+ else:
+ arguments[spec.name] = st.text_input(spec.flag, value=default, help=help_text,
+ key=f"arg-{workflow.key}-{spec.name}")
+
+ if workflow.input_assignments:
+ with st.expander("Input assignments", expanded=True):
+ labels = ["Do not pass"] + [str(path) for path in available_files]
+ for spec in workflow.input_assignments:
+ selected = st.selectbox(spec.label, labels, help=spec.description or None,
+ key=f"input-{workflow.key}-{spec.role}")
+ custom = st.text_input(f"Existing path for {spec.label}", value="",
+ key=f"input-path-{workflow.key}-{spec.role}")
+ if custom.strip():
+ assignments[spec.role] = custom.strip()
+ elif selected != "Do not pass":
+ assignments[spec.role] = selected
+
+ config_paths = [Path(path) for path in assignments.values() if
+ Path(path).suffix.lower() in {".json", ".yaml", ".yml", ".toml", ".txt"}]
+ if config_paths:
+ with st.expander("Config preview", expanded=False):
+ selected_config = st.selectbox("Config file", config_paths, format_func=lambda path: path.name)
+ try:
+ parsed = parse_config_file(selected_config)
+ if isinstance(parsed, (dict, list)):
+ st.json(parsed)
+ else:
+ st.code(str(parsed))
+ except Exception as exc:
+ st.error(f"Could not parse config: {exc}")
+ return arguments, assignments
diff --git a/dashboard/components/console_panel.py b/dashboard/components/console_panel.py
new file mode 100644
index 0000000..54749bb
--- /dev/null
+++ b/dashboard/components/console_panel.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+
+def render_console(lines: list[str], height: int = 360) -> None:
+ """Render streamed ANSI-colored console output in Streamlit."""
+ import streamlit as st
+ from ansi2html import Ansi2HTMLConverter
+
+ raw_text = "".join(lines)
+
+ converter = Ansi2HTMLConverter(
+ inline=True,
+ escaped=True,
+ scheme="ansi2html",
+ )
+ html = converter.convert(raw_text, full=False)
+
+ st.markdown(
+ f"""
+ {html}
+ """,
+ unsafe_allow_html=True,
+ )
+
+
+def render_cancellation_note() -> None:
+ """Explain the current cancellation boundary without exposing unsafe controls."""
+ import streamlit as st
+
+ st.caption(
+ "Cancellation is not exposed in this dashboard phase: Streamlit reruns make reliable foreground "
+ "process termination fragile without a background job supervisor. The runner supports a cancellation "
+ "callback for future supervised execution."
+ )
diff --git a/dashboard/components/download_panel.py b/dashboard/components/download_panel.py
new file mode 100644
index 0000000..ce07271
--- /dev/null
+++ b/dashboard/components/download_panel.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+from hashlib import md5
+from pathlib import Path
+
+from dashboard.file_utils import create_result_zip
+from dashboard.result_parser import ResultInventory
+
+
+def _path_key(path: Path) -> str:
+ return md5(str(path.resolve()).encode("utf-8")).hexdigest()[:12]
+
+
+def _safe_key_part(value: object) -> str:
+ text = str(value)
+ return "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)
+
+
+def _widget_key(prefix: str, *parts: object) -> str:
+ return "::".join([prefix, *(_safe_key_part(part) for part in parts if part is not None)])
+
+
+def render_download_panel(inventory: ResultInventory, key_prefix: str = "download-panel") -> None:
+ import streamlit as st
+
+ root = Path(inventory.root)
+ root_key = _widget_key(key_prefix, _path_key(root))
+ zip_state_key = _widget_key(root_key, "zip-bytes")
+
+ st.subheader("Download")
+ st.caption(
+ "Create a ZIP archive of the selected result directory. "
+ "The archive is generated in memory and not written to disk."
+ )
+
+ if st.button(
+ "Prepare ZIP archive",
+ key=_widget_key(root_key, "prepare-zip"),
+ help="Package all files in this result directory for download.",
+ ):
+ try:
+ st.session_state[zip_state_key] = create_result_zip(root)
+ except (FileNotFoundError, NotADirectoryError, ValueError) as exc:
+ st.error(f"Could not create result ZIP: {exc}")
+ return
+
+ if zip_state_key in st.session_state:
+ st.download_button(
+ "Download result ZIP",
+ data=st.session_state[zip_state_key],
+ file_name=f"{root.name or 'phoskintime_results'}.zip",
+ mime="application/zip",
+ key=_widget_key(root_key, "download-zip"),
+ )
diff --git a/dashboard/components/input_preview.py b/dashboard/components/input_preview.py
new file mode 100644
index 0000000..6bd3714
--- /dev/null
+++ b/dashboard/components/input_preview.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.file_utils import preview_table, read_text_preview, validate_existing_file
+
+
+def render_input_preview(paths: list[Path]) -> None:
+ """Preview uploaded/selected files without modifying them."""
+ import streamlit as st
+
+ st.subheader("Input preview")
+ if not paths:
+ st.info("Upload or select files to preview them here.")
+ return
+ selected = st.selectbox("Preview file", paths, format_func=lambda path: path.name)
+ problems = validate_existing_file(selected)
+ if problems:
+ st.error("; ".join(problems))
+ return
+ if selected.suffix.lower() in {".csv", ".tsv", ".xlsx"}:
+ try:
+ st.dataframe(preview_table(selected), use_container_width=True)
+ except Exception as exc:
+ st.error(f"Could not preview table. Check that the file is a readable CSV, TSV, or Excel workbook: {exc}")
+ else:
+ try:
+ text, truncated = read_text_preview(selected)
+ except UnicodeDecodeError as exc:
+ st.error(f"Could not preview text file: {exc}")
+ return
+ st.code(text)
+ if truncated:
+ st.caption("Preview truncated for display.")
diff --git a/dashboard/components/log_viewer.py b/dashboard/components/log_viewer.py
new file mode 100644
index 0000000..0566bee
--- /dev/null
+++ b/dashboard/components/log_viewer.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.file_utils import DisplayFile, read_text_preview
+
+
+def render_text_file(label: str, path: Path | None, language: str | None = None) -> None:
+ import streamlit as st
+
+ st.subheader(label)
+ if path is None:
+ st.info(f"No {label} file was found in this result directory.")
+ return
+ text, truncated = read_text_preview(path)
+ st.code(text, language=language)
+ if truncated:
+ st.caption("Preview truncated for display; download the file to inspect all content.")
+
+
+def render_logs(logs: list[DisplayFile]) -> None:
+ import streamlit as st
+
+ st.subheader("Logs")
+ if not logs:
+ st.info("No log files were found under logs/ or console.log.")
+ return
+ selected = st.selectbox("Log file", logs, format_func=lambda item: item.relative_path)
+ text, truncated = read_text_preview(selected.path)
+ st.text_area(selected.relative_path, text, height=420)
+ if truncated:
+ st.caption("Preview truncated for display; download the file to inspect all content.")
diff --git a/dashboard/components/metadata_viewer.py b/dashboard/components/metadata_viewer.py
new file mode 100644
index 0000000..c246944
--- /dev/null
+++ b/dashboard/components/metadata_viewer.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+from dashboard.file_utils import read_text_preview
+
+
+def render_metadata(path: Path | None) -> None:
+ """Render metadata.json with a clear missing-state message."""
+ import streamlit as st
+
+ st.subheader("Metadata")
+ if path is None:
+ st.info(
+ "No metadata.json file was found. The run can still be browsed, but provenance details are unavailable.")
+ return
+ try:
+ st.json(json.loads(path.read_text(encoding="utf-8")))
+ except json.JSONDecodeError:
+ text, truncated = read_text_preview(path)
+ st.warning("metadata.json is not valid JSON; showing raw content instead.")
+ st.code(text, language="json")
+ if truncated:
+ st.caption("Preview truncated for display.")
diff --git a/dashboard/components/plot_viewer.py b/dashboard/components/plot_viewer.py
new file mode 100644
index 0000000..bc99881
--- /dev/null
+++ b/dashboard/components/plot_viewer.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from dashboard.file_utils import DisplayFile, human_size, read_text_preview
+
+
+def render_plots(plots: list[DisplayFile]) -> None:
+ import streamlit as st
+ import streamlit.components.v1 as components
+
+ st.subheader("Plots")
+ if not plots:
+ st.info("No PNG, JPG, JPEG, SVG, or HTML plots were found in plots/ or recognised legacy plot folders.")
+ return
+ selected = st.selectbox("Plot", plots,
+ format_func=lambda item: f"{item.relative_path} ({human_size(item.size_bytes)})")
+ if selected.suffix in {".png", ".jpg", ".jpeg", ".svg"}:
+ st.image(str(selected.path), caption=selected.relative_path, use_container_width=True)
+ elif selected.suffix in {".html", ".htm"}:
+ html, truncated = read_text_preview(selected.path, max_bytes=2_000_000)
+ components.html(html, height=700, scrolling=True)
+ if truncated:
+ st.warning("HTML preview was truncated because the file is large. Download the file for the full plot.")
+ else:
+ st.info("This plot type can be downloaded but is not previewed inline.")
diff --git a/dashboard/components/preset_panel.py b/dashboard/components/preset_panel.py
new file mode 100644
index 0000000..0b9218a
--- /dev/null
+++ b/dashboard/components/preset_panel.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+from dashboard.config_utils import DashboardSelection, save_preset
+
+
+def render_preset_panel(selection: DashboardSelection, repo_root: Path) -> None:
+ """Allow saving the resolved dashboard selections as a preset file."""
+ import streamlit as st
+
+ st.subheader("Preset")
+ preset_name = st.text_input("Preset filename", value=f"{selection.run_name}.json",
+ help="Save the current dashboard selections for reproducibility.")
+ if st.button("Save preset"):
+ target = repo_root / "dashboard_uploads" / selection.run_name / preset_name
+ try:
+ path = save_preset(selection, target)
+ except ValueError as exc:
+ st.error(str(exc))
+ return
+ st.success(f"Saved preset to {path}")
+ st.download_button(
+ "Download preset JSON",
+ data=json.dumps(selection.to_dict(), indent=2, sort_keys=True),
+ file_name=f"{selection.run_name}.json",
+ mime="application/json",
+ )
diff --git a/dashboard/components/report_viewer.py b/dashboard/components/report_viewer.py
new file mode 100644
index 0000000..c5703e9
--- /dev/null
+++ b/dashboard/components/report_viewer.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+import base64
+
+from dashboard.file_utils import DisplayFile, human_size, read_text_preview
+
+
+def render_reports(reports: list[DisplayFile]) -> None:
+ """Render report files where Streamlit can preview them."""
+ import streamlit as st
+ import streamlit.components.v1 as components
+
+ st.subheader("Reports")
+ if not reports:
+ st.info("No HTML, Markdown, or PDF reports were found in reports/.")
+ return
+ selected = st.selectbox("Report", reports,
+ format_func=lambda item: f"{item.relative_path} ({human_size(item.size_bytes)})")
+ if selected.suffix in {".html", ".htm"}:
+ html, truncated = read_text_preview(selected.path, max_bytes=2_000_000)
+ components.html(html, height=700, scrolling=True)
+ if truncated:
+ st.warning(
+ "HTML report preview was truncated because the file is large. Download the file for the full report.")
+ elif selected.suffix == ".md":
+ markdown, truncated = read_text_preview(selected.path, max_bytes=1_000_000)
+ st.markdown(markdown)
+ if truncated:
+ st.warning(
+ "Markdown report preview was truncated because the file is large. Download the file for the full report.")
+ elif selected.suffix == ".pdf":
+ pdf_bytes = selected.path.read_bytes()
+ encoded = base64.b64encode(pdf_bytes).decode("ascii")
+ components.html(
+ f'',
+ height=720,
+ scrolling=True,
+ )
+ with selected.path.open("rb") as handle:
+ st.download_button("Download report", data=handle.read(), file_name=selected.name)
diff --git a/dashboard/components/result_browser.py b/dashboard/components/result_browser.py
new file mode 100644
index 0000000..d9da5d8
--- /dev/null
+++ b/dashboard/components/result_browser.py
@@ -0,0 +1,177 @@
+from __future__ import annotations
+
+from hashlib import md5
+from pathlib import Path
+
+from dashboard.components.download_panel import render_download_panel
+from dashboard.components.log_viewer import render_logs, render_text_file
+from dashboard.components.metadata_viewer import render_metadata
+from dashboard.components.plot_viewer import render_plots
+from dashboard.components.report_viewer import render_reports
+from dashboard.components.table_viewer import render_tables
+from dashboard.registry import infer_workflow
+from dashboard.result_parser import ResultInventory, discover_result_directory
+
+
+def _path_key(path: Path) -> str:
+ return md5(str(path.resolve()).encode("utf-8")).hexdigest()[:12]
+
+
+def _safe_key_part(value: object) -> str:
+ text = str(value)
+ return "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)
+
+
+def _widget_key(prefix: str, *parts: object) -> str:
+ return "::".join([prefix, *(_safe_key_part(part) for part in parts if part is not None)])
+
+
+def _summary_metric(label: str, value: int) -> None:
+ import streamlit as st
+
+ st.metric(label, value)
+
+
+def render_result_browser(inventory: ResultInventory, key_prefix: str = "results-browser") -> None:
+ """Render all discovered result-directory content."""
+ import streamlit as st
+
+ root_key = _widget_key(key_prefix, _path_key(inventory.root))
+
+ workflow = infer_workflow(inventory)
+ st.header("Result browser")
+ st.caption(f"Directory: `{inventory.root}`")
+ st.info(f"Detected workflow: **{workflow.label}** — {workflow.description}")
+
+ columns = st.columns(6)
+ with columns[0]:
+ _summary_metric("Tables", len(inventory.tables))
+ with columns[1]:
+ _summary_metric("Plots", len(inventory.plots))
+ with columns[2]:
+ _summary_metric("Logs", len(inventory.logs))
+ with columns[3]:
+ _summary_metric("Reports", len(inventory.reports))
+ with columns[4]:
+ _summary_metric("Artifacts", len(inventory.artifacts))
+ with columns[5]:
+ _summary_metric("Child runs", len(inventory.child_runs))
+
+ if inventory.missing_expected:
+ with st.expander(
+ "Missing standard contract files/folders",
+ expanded=False,
+ ):
+ st.write(
+ "The browser can still show recognised legacy outputs, "
+ "but these standard items were not found:"
+ )
+ st.code("\n".join(inventory.missing_expected))
+
+ tabs = st.tabs(
+ [
+ "Metadata",
+ "Tables",
+ "Plots",
+ "Logs",
+ "Reports",
+ "Artifacts",
+ "Child runs",
+ "Download",
+ ]
+ )
+
+ with tabs[0]:
+ render_metadata(inventory.metadata)
+ render_text_file("Command", inventory.command, language="bash")
+ render_text_file("Resolved config", inventory.config, language="yaml")
+
+ with tabs[1]:
+ render_tables(inventory.tables)
+
+ with tabs[2]:
+ render_plots(inventory.plots)
+
+ with tabs[3]:
+ render_logs(inventory.logs)
+
+ with tabs[4]:
+ render_reports(inventory.reports)
+
+ with tabs[5]:
+ _render_downloadable_list(
+ "Artifacts",
+ inventory.artifacts,
+ key_prefix=_widget_key(root_key, "artifacts"),
+ )
+
+ with tabs[6]:
+ _render_child_runs(
+ inventory,
+ key_prefix=_widget_key(root_key, "child-runs"),
+ )
+
+ with tabs[7]:
+ render_download_panel(
+ inventory,
+ key_prefix=_widget_key(root_key, "download"),
+ )
+
+
+def _render_downloadable_list(label: str, files, key_prefix: str) -> None:
+ import streamlit as st
+
+ st.subheader(label)
+ if not files:
+ st.info(f"No {label.lower()} were found in the selected result directory.")
+ return
+
+ selected = st.selectbox(
+ label,
+ files,
+ format_func=lambda item: item.relative_path,
+ key=_widget_key(key_prefix, "select"),
+ )
+
+ st.write(f"Selected: `{selected.relative_path}`")
+
+ with selected.path.open("rb") as handle:
+ st.download_button(
+ "Download",
+ data=handle.read(),
+ file_name=selected.name,
+ key=_widget_key(key_prefix, "download", selected.relative_path),
+ )
+
+
+def _render_child_runs(inventory: ResultInventory, key_prefix: str) -> None:
+ import streamlit as st
+
+ st.subheader("Child workflow result folders")
+ if not inventory.child_runs:
+ st.info("No nested workflow result folders were found in the selected directory.")
+ return
+
+ selected = st.selectbox(
+ "Select child workflow result",
+ inventory.child_runs,
+ format_func=lambda path: (
+ path.relative_to(inventory.root).as_posix()
+ if path.is_relative_to(inventory.root)
+ else str(path)
+ ),
+ key=_widget_key(key_prefix, "select", _path_key(inventory.root)),
+ )
+
+ st.caption(f"Opening `{selected}`")
+
+ try:
+ child_inventory = discover_result_directory(selected)
+ except (FileNotFoundError, NotADirectoryError) as exc:
+ st.warning(f"Child result directory could not be opened: {exc}")
+ return
+
+ render_result_browser(
+ child_inventory,
+ key_prefix=_widget_key(key_prefix, "child", _path_key(selected)),
+ )
diff --git a/dashboard/components/run_status.py b/dashboard/components/run_status.py
new file mode 100644
index 0000000..6176138
--- /dev/null
+++ b/dashboard/components/run_status.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+
+def render_run_status(status: str | None, returncode: int | None = None) -> None:
+ """Render workflow completion status."""
+ import streamlit as st
+
+ if status is None:
+ st.info("No workflow has been run in this session.")
+ elif status == "running":
+ st.warning("Workflow is running…")
+ elif status == "success":
+ st.success("Workflow completed successfully.")
+ elif status == "cancelled":
+ st.warning(f"Workflow was cancelled. Return code: {returncode}")
+ else:
+ st.error(f"Workflow failed. Return code: {returncode}. Review the console log tail for details.")
diff --git a/dashboard/components/table_viewer.py b/dashboard/components/table_viewer.py
new file mode 100644
index 0000000..82cbca9
--- /dev/null
+++ b/dashboard/components/table_viewer.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+from typing import Any
+
+from dashboard.file_utils import DisplayFile, human_size
+
+
+def _read_table(path: str, suffix: str, sheet_name: str | None = None) -> Any:
+ import pandas as pd
+
+ if suffix == ".csv":
+ return pd.read_csv(path)
+ if suffix == ".tsv":
+ return pd.read_csv(path, sep="\t")
+ if suffix in {".xlsx", ".xls"}:
+ return pd.read_excel(path, sheet_name=sheet_name or 0)
+ raise ValueError(f"Unsupported table suffix: {suffix}")
+
+
+def _excel_sheets(path: str) -> list[str]:
+ import pandas as pd
+
+ return pd.ExcelFile(path).sheet_names
+
+
+def render_tables(tables: list[DisplayFile]) -> None:
+ import streamlit as st
+
+ st.subheader("Tables")
+ if not tables:
+ st.info("No CSV, TSV, or Excel tables were found in tables/ or recognised legacy locations.")
+ return
+ selected = st.selectbox("Table", tables,
+ format_func=lambda item: f"{item.relative_path} ({human_size(item.size_bytes)})")
+ sheet_name = None
+ if selected.suffix in {".xlsx", ".xls"}:
+ try:
+ sheets = _excel_sheets(str(selected.path))
+ except Exception as exc:
+ st.error(f"Could not inspect Excel workbook {selected.relative_path}: {exc}")
+ return
+ sheet_name = st.selectbox("Sheet", sheets)
+ try:
+ st.dataframe(_read_table(str(selected.path), selected.suffix, sheet_name), use_container_width=True)
+ except Exception as exc:
+ st.error(
+ f"Could not load table {selected.relative_path}. Check that it is a readable CSV, TSV, or Excel file: {exc}")
diff --git a/dashboard/components/upload_panel.py b/dashboard/components/upload_panel.py
new file mode 100644
index 0000000..da59f93
--- /dev/null
+++ b/dashboard/components/upload_panel.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.file_utils import (
+ UPLOAD_EXTENSIONS,
+ create_upload_dir,
+ detect_duplicate_filenames,
+ save_uploaded_file,
+ validate_upload_filename,
+)
+
+
+def render_upload_panel(repo_root: Path, run_name: str) -> list[Path]:
+ """Render upload controls and save files under dashboard_uploads// on demand."""
+ import streamlit as st
+
+ st.subheader("Upload inputs")
+ uploaded = st.file_uploader(
+ "Upload input/config files",
+ type=sorted(ext.lstrip(".") for ext in UPLOAD_EXTENSIONS),
+ accept_multiple_files=True,
+ help="Files are copied to dashboard_uploads// and are not written into data/ automatically.",
+ )
+ saved: list[Path] = []
+ if not uploaded:
+ return saved
+
+ duplicates = detect_duplicate_filenames(file.name for file in uploaded)
+ if duplicates:
+ st.error("Duplicate filenames after sanitization: " + ", ".join(duplicates))
+ return saved
+
+ upload_dir = create_upload_dir(repo_root, run_name)
+ for file in uploaded:
+ problems = validate_upload_filename(file.name)
+ if problems:
+ st.error("; ".join(problems))
+ continue
+ try:
+ path = save_uploaded_file(file, upload_dir)
+ except ValueError as exc:
+ st.error(str(exc))
+ continue
+ saved.append(path)
+ if saved:
+ st.success(f"Saved {len(saved)} file(s) to {upload_dir}")
+ return saved
diff --git a/dashboard/components/validation_panel.py b/dashboard/components/validation_panel.py
new file mode 100644
index 0000000..3a7d5d0
--- /dev/null
+++ b/dashboard/components/validation_panel.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from dashboard.config_utils import validate_input_assignments
+from dashboard.file_utils import detect_duplicate_filenames, validate_existing_file
+from dashboard.registry import WorkflowDescriptor
+
+
+def validate_dashboard_setup(workflow: WorkflowDescriptor, available_paths, assignments: dict[str, str]) -> list[str]:
+ """Validate uploaded/selected files and workflow input assignments."""
+ problems: list[str] = []
+ duplicates = detect_duplicate_filenames(path.name for path in available_paths)
+ if duplicates:
+ problems.append("Duplicate filenames: " + ", ".join(duplicates))
+ for path in available_paths:
+ problems.extend(f"{path.name}: {problem}" for problem in validate_existing_file(path))
+ problems.extend(validate_input_assignments(workflow, assignments))
+ return problems
+
+
+def render_validation_panel(problems: list[str]) -> bool:
+ """Render validation status and return whether running should be enabled."""
+ import streamlit as st
+
+ st.subheader("Validation")
+ if problems:
+ st.error("Resolve validation issues before running.")
+ for problem in problems:
+ st.write(f"- {problem}")
+ return False
+ st.success("No validation issues detected for the current dashboard selections.")
+ return True
diff --git a/dashboard/components/workflow_selector.py b/dashboard/components/workflow_selector.py
new file mode 100644
index 0000000..86d82b8
--- /dev/null
+++ b/dashboard/components/workflow_selector.py
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.registry import WorkflowDescriptor, launchable_workflows, pixi_environments
+
+
+def render_workflow_selector(repo_root: Path) -> tuple[WorkflowDescriptor, str, str]:
+ """Render workflow, Pixi environment, and run-name controls."""
+ import streamlit as st
+
+ workflows = launchable_workflows()
+ workflow = st.selectbox("Workflow", workflows, format_func=lambda item: f"{item.label} ({item.key})")
+ st.caption(workflow.description)
+ if workflow.required_inputs:
+ st.write("Known inputs:", ", ".join(workflow.required_inputs))
+
+ envs = pixi_environments(repo_root / "pixi.toml")
+ env = st.selectbox("Pixi environment", envs, index=0, help="Choose a Pixi environment defined in pixi.toml.")
+ run_name = st.text_input("Run name", value=f"{workflow.key}-run",
+ help="Used to name the result and upload folders; unsafe characters are sanitized.")
+ return workflow, env, run_name
diff --git a/dashboard/components/workflow_tabs.py b/dashboard/components/workflow_tabs.py
new file mode 100644
index 0000000..0e43324
--- /dev/null
+++ b/dashboard/components/workflow_tabs.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+
+def render_workflow_tabs(result_dir: str | Path) -> None:
+ """Render workflow-specific tabs for a selected result directory."""
+ import streamlit as st
+
+ from dashboard.workflow_panels import analysis, kinopt, networkmodel, protwise, tfopt
+
+ root = Path(result_dir)
+ tabs = st.tabs(["KinOpt", "TFOpt", "ProtWise", "Networkmodel", "Advanced analysis"])
+ with tabs[0]:
+ kinopt.render(root)
+ with tabs[1]:
+ tfopt.render(root)
+ with tabs[2]:
+ protwise.render(root)
+ with tabs[3]:
+ networkmodel.render(root)
+ with tabs[4]:
+ analysis.render(root)
diff --git a/dashboard/config_utils.py b/dashboard/config_utils.py
new file mode 100644
index 0000000..f2bd2f3
--- /dev/null
+++ b/dashboard/config_utils.py
@@ -0,0 +1,149 @@
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+
+@dataclass(frozen=True)
+class DashboardSelection:
+ """Serializable dashboard workflow setup selections."""
+
+ workflow_key: str
+ run_name: str
+ pixi_environment: str = "default"
+ arguments: dict[str, Any] = field(default_factory=dict)
+ input_assignments: dict[str, str] = field(default_factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "workflow_key": self.workflow_key,
+ "run_name": self.run_name,
+ "pixi_environment": self.pixi_environment,
+ "arguments": self.arguments,
+ "input_assignments": self.input_assignments,
+ }
+
+
+def _parse_simple_yaml(text: str) -> dict[str, Any]:
+ """Parse a conservative flat YAML subset when PyYAML is unavailable."""
+ data: dict[str, Any] = {}
+ for raw_line in text.splitlines():
+ line = raw_line.strip()
+ if not line or line.startswith("#"):
+ continue
+ if ":" not in line:
+ raise ValueError("Only simple key: value YAML is supported without PyYAML")
+ key, value = line.split(":", 1)
+ value = value.strip()
+ if value.lower() in {"true", "false"}:
+ parsed: Any = value.lower() == "true"
+ elif value == "":
+ parsed = None
+ else:
+ try:
+ parsed = int(value)
+ except ValueError:
+ try:
+ parsed = float(value)
+ except ValueError:
+ parsed = value.strip('"\'')
+ data[key.strip()] = parsed
+ return data
+
+
+def parse_config_file(path: str | Path) -> Any:
+ """Parse supported dashboard config/preset files for preview."""
+ file_path = Path(path)
+ suffix = file_path.suffix.lower()
+ text = file_path.read_text(encoding="utf-8")
+ if suffix == ".json":
+ return json.loads(text)
+ if suffix in {".yaml", ".yml"}:
+ try:
+ import yaml # type: ignore
+ return yaml.safe_load(text)
+ except ImportError:
+ return _parse_simple_yaml(text)
+ if suffix == ".toml":
+ import tomllib
+ with file_path.open("rb") as handle:
+ return tomllib.load(handle)
+ if suffix == ".txt":
+ return text
+ raise ValueError(f"Unsupported config extension: {suffix or ''}")
+
+
+def selection_to_preset(selection: DashboardSelection) -> dict[str, Any]:
+ """Convert dashboard selections to a preset dictionary."""
+ return selection.to_dict()
+
+
+def save_preset(selection: DashboardSelection, path: str | Path) -> Path:
+ """Save dashboard selections as JSON or simple YAML based on extension."""
+ target = Path(path)
+ target.parent.mkdir(parents=True, exist_ok=True)
+ data = selection_to_preset(selection)
+ suffix = target.suffix.lower()
+ if suffix == ".json":
+ target.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ elif suffix in {".yaml", ".yml"}:
+ try:
+ import yaml # type: ignore
+ text = yaml.safe_dump(data, sort_keys=True)
+ except ImportError:
+ lines = [f"{key}: {json.dumps(value)}" for key, value in data.items()]
+ text = "\n".join(lines) + "\n"
+ target.write_text(text, encoding="utf-8")
+ else:
+ raise ValueError("Presets must be saved as .json, .yaml, or .yml")
+ return target
+
+
+def load_preset(path: str | Path) -> DashboardSelection:
+ """Load a saved dashboard selection preset."""
+ parsed = parse_config_file(path)
+ if not isinstance(parsed, dict):
+ raise ValueError("Preset must contain a mapping/object")
+ return DashboardSelection(
+ workflow_key=str(parsed.get("workflow_key", "")),
+ run_name=str(parsed.get("run_name", "run")),
+ pixi_environment=str(parsed.get("pixi_environment", "default")),
+ arguments=dict(parsed.get("arguments", {}) or {}),
+ input_assignments=dict(parsed.get("input_assignments", {}) or {}),
+ )
+
+
+def validate_input_assignments(workflow, assignments: dict[str, str]) -> list[str]:
+ """Validate assigned input roles against workflow specs and basic file rules."""
+ problems: list[str] = []
+ specs = {spec.role: spec for spec in workflow.input_assignments}
+ for role in sorted(set(assignments) - set(specs)):
+ problems.append(f"Unsupported input role for {workflow.key}: {role}")
+ for spec in workflow.input_assignments:
+ raw_path = assignments.get(spec.role, "")
+ if spec.required and not raw_path:
+ problems.append(f"Missing required input: {spec.label}")
+ continue
+ if not raw_path:
+ continue
+ path = Path(raw_path)
+ if spec.argument_name is None:
+ if not path.exists():
+ problems.append(f"{spec.label} does not exist: {path}")
+ continue
+ if not path.exists():
+ problems.append(f"{spec.label} does not exist: {path}")
+ continue
+ if not path.is_file():
+ problems.append(f"{spec.label} is not a file: {path}")
+ continue
+ if spec.extensions and path.suffix.lower() not in spec.extensions:
+ problems.append(f"{spec.label} must use one of {', '.join(spec.extensions)}")
+ try:
+ if path.stat().st_size == 0:
+ problems.append(f"{spec.label}: File is empty")
+ except OSError as exc:
+ problems.append(f"{spec.label}: Unreadable file: {exc}")
+ return problems
diff --git a/dashboard/file_utils.py b/dashboard/file_utils.py
new file mode 100644
index 0000000..2803af7
--- /dev/null
+++ b/dashboard/file_utils.py
@@ -0,0 +1,207 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from io import BytesIO
+from pathlib import Path
+from typing import Iterable
+from zipfile import ZIP_DEFLATED, ZipFile
+
+TABLE_EXTENSIONS = {".csv", ".tsv", ".xlsx", ".xls"}
+IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".svg"}
+HTML_EXTENSIONS = {".html", ".htm"}
+PLOT_EXTENSIONS = IMAGE_EXTENSIONS | HTML_EXTENSIONS
+REPORT_EXTENSIONS = HTML_EXTENSIONS | {".md", ".pdf"}
+TEXT_EXTENSIONS = {".txt", ".log", ".yaml", ".yml", ".toml", ".json", ".md", ".csv", ".tsv"}
+
+EXCLUDED_ZIP_PARTS = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".git"}
+
+UPLOAD_EXTENSIONS = {".csv", ".tsv", ".xlsx", ".yaml", ".yml", ".toml", ".json", ".txt"}
+_FILENAME_SAFE_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")
+
+
+@dataclass(frozen=True)
+class DisplayFile:
+ """A discovered result file with paths suitable for UI labels and reading."""
+
+ path: Path
+ root: Path
+ category: str
+
+ @property
+ def name(self) -> str:
+ return self.path.name
+
+ @property
+ def relative_path(self) -> str:
+ try:
+ return self.path.relative_to(self.root).as_posix()
+ except ValueError:
+ return self.path.as_posix()
+
+ @property
+ def suffix(self) -> str:
+ return self.path.suffix.lower()
+
+ @property
+ def size_bytes(self) -> int:
+ try:
+ return self.path.stat().st_size
+ except OSError:
+ return 0
+
+
+def resolve_directory(path: str | Path) -> Path:
+ """Resolve an existing result directory path."""
+ directory = Path(path).expanduser().resolve()
+ if not directory.exists():
+ raise FileNotFoundError(f"Result directory does not exist: {directory}")
+ if not directory.is_dir():
+ raise NotADirectoryError(f"Result path is not a directory: {directory}")
+ return directory
+
+
+def iter_files(directory: Path, patterns: Iterable[str] = ("*",), recursive: bool = False) -> list[Path]:
+ """Return sorted files matching one or more glob patterns without reading file contents."""
+ if not directory.is_dir():
+ return []
+ matches: set[Path] = set()
+ for pattern in patterns:
+ iterator = directory.rglob(pattern) if recursive else directory.glob(pattern)
+ matches.update(path for path in iterator if path.is_file())
+ return sorted(matches, key=lambda path: path.as_posix().lower())
+
+
+def filter_by_suffix(paths: Iterable[Path], suffixes: set[str]) -> list[Path]:
+ """Filter paths by lower-case suffix and sort deterministically."""
+ return sorted((p for p in paths if p.suffix.lower() in suffixes), key=lambda path: path.as_posix().lower())
+
+
+def read_text_preview(path: Path, max_bytes: int = 512_000) -> tuple[str, bool]:
+ """Read a bounded text preview and return whether truncation occurred."""
+ size = path.stat().st_size
+ with path.open("rb") as handle:
+ raw = handle.read(max_bytes + 1)
+ truncated = len(raw) > max_bytes or size > max_bytes
+ if truncated:
+ raw = raw[:max_bytes]
+ return raw.decode("utf-8", errors="replace"), truncated
+
+
+def create_result_zip(root: str | Path) -> bytes:
+ """Create an in-memory ZIP archive for a result directory."""
+ directory = resolve_directory(root)
+ buffer = BytesIO()
+ with ZipFile(buffer, mode="w", compression=ZIP_DEFLATED) as archive:
+ for path in sorted(directory.rglob("*"), key=lambda p: p.as_posix().lower()):
+ if not path.is_file():
+ continue
+ rel = path.relative_to(directory)
+ if any(part in EXCLUDED_ZIP_PARTS for part in rel.parts):
+ continue
+ archive.write(path, rel.as_posix())
+ return buffer.getvalue()
+
+
+def human_size(num_bytes: int) -> str:
+ """Format a byte count for display."""
+ value = float(num_bytes)
+ for unit in ("B", "KB", "MB", "GB"):
+ if value < 1024 or unit == "GB":
+ return f"{value:.1f} {unit}" if unit != "B" else f"{int(value)} B"
+ value /= 1024
+ return f"{num_bytes} B"
+
+
+def sanitize_filename(filename: str) -> str:
+ """Return a safe filename while preserving a supported extension when present."""
+ raw = Path(filename).name.strip().replace(" ", "-")
+ cleaned = "".join(ch if ch in _FILENAME_SAFE_CHARS else "-" for ch in raw).strip(".-_")
+ return cleaned or "uploaded-file"
+
+
+def create_upload_dir(repo_root: str | Path, run_id: str, base_dir: str | Path = "dashboard_uploads") -> Path:
+ """Create a per-run dashboard upload directory under the repository by default."""
+ from dashboard.command_builder import sanitize_run_name
+
+ root = Path(repo_root).resolve()
+ base = Path(base_dir).expanduser()
+ if not base.is_absolute():
+ base = root / base
+ directory = (base / sanitize_run_name(run_id)).resolve()
+ directory.mkdir(parents=True, exist_ok=True)
+ return directory
+
+
+def detect_duplicate_filenames(filenames: Iterable[str]) -> list[str]:
+ """Return sanitized duplicate upload filenames."""
+ seen: set[str] = set()
+ duplicates: set[str] = set()
+ for name in filenames:
+ safe = sanitize_filename(name)
+ if safe in seen:
+ duplicates.add(safe)
+ seen.add(safe)
+ return sorted(duplicates)
+
+
+def validate_upload_filename(filename: str) -> list[str]:
+ """Validate a dashboard upload filename without reading content."""
+ safe = sanitize_filename(filename)
+ suffix = Path(safe).suffix.lower()
+ if suffix not in UPLOAD_EXTENSIONS:
+ return [f"Unsupported extension for {filename!r}: {suffix or ''}"]
+ return []
+
+
+def save_uploaded_file(uploaded_file, upload_dir: str | Path) -> Path:
+ """Save a Streamlit-style uploaded file into the run upload directory."""
+ directory = Path(upload_dir)
+ directory.mkdir(parents=True, exist_ok=True)
+ name = sanitize_filename(getattr(uploaded_file, "name", "uploaded-file"))
+ problems = validate_upload_filename(name)
+ if problems:
+ raise ValueError("; ".join(problems))
+ target = directory / name
+ if hasattr(uploaded_file, "getbuffer"):
+ data = bytes(uploaded_file.getbuffer())
+ elif hasattr(uploaded_file, "read"):
+ data = uploaded_file.read()
+ else:
+ data = bytes(uploaded_file)
+ if not data:
+ raise ValueError(f"Uploaded file is empty: {name}")
+ target.write_bytes(data)
+ return target
+
+
+def validate_existing_file(path: str | Path) -> list[str]:
+ """Detect basic file problems for saved uploads or selected paths."""
+ file_path = Path(path)
+ problems: list[str] = []
+ if not file_path.exists():
+ return [f"File does not exist: {file_path}"]
+ if not file_path.is_file():
+ return [f"Path is not a file: {file_path}"]
+ if file_path.suffix.lower() not in UPLOAD_EXTENSIONS:
+ problems.append(f"Unsupported extension: {file_path.suffix.lower() or ''}")
+ try:
+ if file_path.stat().st_size == 0:
+ problems.append("File is empty")
+ except OSError as exc:
+ problems.append(f"Unreadable file: {exc}")
+ return problems
+
+
+def preview_table(path: str | Path, max_rows: int = 50, sheet_name: str | int | None = 0):
+ """Read a bounded preview of CSV/TSV/XLSX files using pandas."""
+ import pandas as pd
+
+ file_path = Path(path)
+ suffix = file_path.suffix.lower()
+ if suffix == ".csv":
+ return pd.read_csv(file_path, nrows=max_rows)
+ if suffix == ".tsv":
+ return pd.read_csv(file_path, sep="\t", nrows=max_rows)
+ if suffix == ".xlsx":
+ return pd.read_excel(file_path, sheet_name=0 if sheet_name is None else sheet_name, nrows=max_rows)
+ raise ValueError(f"Preview is not supported for {suffix or ''} files")
diff --git a/dashboard/registry.py b/dashboard/registry.py
new file mode 100644
index 0000000..8726a5d
--- /dev/null
+++ b/dashboard/registry.py
@@ -0,0 +1,295 @@
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Literal
+
+from dashboard.result_parser import ResultInventory
+
+ArgKind = Literal["str", "int", "float", "bool", "path"]
+
+
+@dataclass(frozen=True)
+class ArgumentSpec:
+ """Structured argument accepted by a registered workflow launcher."""
+
+ name: str
+ flag: str
+ kind: ArgKind = "str"
+ description: str = ""
+ default: str | int | float | bool | None = None
+ required: bool = False
+
+
+@dataclass(frozen=True)
+class InputSpec:
+ """Workflow input role that can be assigned from uploads or selected files."""
+
+ role: str
+ label: str
+ argument_name: str | None = None
+ description: str = ""
+ required: bool = False
+ extensions: tuple[str, ...] = ()
+
+
+@dataclass(frozen=True)
+class WorkflowDescriptor:
+ key: str
+ label: str
+ description: str
+ pixi_task: str | None = None
+ python_module: str | None = None
+ module_args: tuple[str, ...] = ()
+ accepted_arguments: tuple[ArgumentSpec, ...] = ()
+ required_inputs: tuple[str, ...] = ()
+ input_assignments: tuple[InputSpec, ...] = ()
+ output_dir_arg: str | None = "--outdir"
+ expected_output_folder: str | None = None
+ result_workflow_keys: tuple[str, ...] = field(default_factory=tuple)
+ safe_for_dashboard: bool = True
+
+
+WORKFLOWS: dict[str, WorkflowDescriptor] = {
+ # Result-browser descriptors retained for backwards-compatible metadata inference.
+ "kinopt.local": WorkflowDescriptor(
+ "kinopt.local", "KinOpt local", "Local kinase-phosphorylation optimisation results.",
+ result_workflow_keys=("kinopt.local",), expected_output_folder="results",
+ ),
+ "tfopt.local": WorkflowDescriptor(
+ "tfopt.local", "TFOpt local", "Local transcription-factor optimisation results.",
+ result_workflow_keys=("tfopt.local",), expected_output_folder="results",
+ ),
+ "protwise.runner": WorkflowDescriptor(
+ "protwise.runner", "ProtWise", "ProtWise ODE modelling results.",
+ result_workflow_keys=("protwise.runner",), expected_output_folder="results",
+ ),
+ "networkmodel.runner": WorkflowDescriptor(
+ "networkmodel.runner", "Network model", "Integrated global network model results.",
+ result_workflow_keys=("networkmodel.runner",), expected_output_folder="results",
+ ),
+ "unknown": WorkflowDescriptor(
+ "unknown", "Unknown workflow", "Result directory without recognised workflow metadata.",
+ output_dir_arg=None, safe_for_dashboard=False,
+ ),
+ # Launchable workflows for Phase 2.
+ "prep": WorkflowDescriptor(
+ key="prep",
+ label="Preprocessing",
+ description="Run preprocessing cleanup using the existing processing.cleanup module.",
+ pixi_task="prep",
+ python_module="processing.cleanup",
+ output_dir_arg=None,
+ expected_output_folder="results/prep",
+ result_workflow_keys=("prep",),
+ ),
+ "kinopt-local": WorkflowDescriptor(
+ key="kinopt-local",
+ label="KinOpt local",
+ description="Run local kinase-phosphorylation optimisation via kinopt.local.",
+ pixi_task="kinopt-local",
+ python_module="kinopt.local",
+ accepted_arguments=(
+ ArgumentSpec("conf", "--conf", "path", "Optional TOML configuration file."),
+ ArgumentSpec("lower_bound", "--lower_bound", "float", "Lower optimisation bound."),
+ ArgumentSpec("upper_bound", "--upper_bound", "float", "Upper optimisation bound."),
+ ArgumentSpec("loss_type", "--loss_type", "str", "Loss function name."),
+ ArgumentSpec("method", "--method", "str", "Optimisation method."),
+ ),
+ required_inputs=("input1.csv", "input2.csv"),
+ input_assignments=(
+ InputSpec("config", "TOML config file", "conf", "Optional kinopt TOML config file.", extensions=(".toml",)),
+ ),
+ output_dir_arg="--outdir",
+ expected_output_folder="results/kinopt-local",
+ result_workflow_keys=("kinopt.local",),
+ ),
+ "tfopt-local": WorkflowDescriptor(
+ key="tfopt-local",
+ label="TFOpt local",
+ description="Run local transcription-factor optimisation via tfopt.local.",
+ pixi_task="tfopt-local",
+ python_module="tfopt.local",
+ accepted_arguments=(
+ ArgumentSpec("conf", "--conf", "path", "Optional TOML configuration file."),
+ ArgumentSpec("lower_bound", "--lower_bound", "float", "Lower optimisation bound."),
+ ArgumentSpec("upper_bound", "--upper_bound", "float", "Upper optimisation bound."),
+ ArgumentSpec("loss_type", "--loss_type", "int", "Loss function identifier."),
+ ),
+ required_inputs=("input1.csv", "input3.csv", "input4.csv"),
+ input_assignments=(
+ InputSpec("config", "TOML config file", "conf", "Optional tfopt TOML config file.", extensions=(".toml",)),
+ ),
+ output_dir_arg="--outdir",
+ expected_output_folder="results/tfopt-local",
+ result_workflow_keys=("tfopt.local",),
+ ),
+ "protwise-model": WorkflowDescriptor(
+ key="protwise-model",
+ label="ProtWise model",
+ description="Run the ProtWise ODE model via protwise.runner.main.",
+ pixi_task="model",
+ python_module="protwise.runner.main",
+ accepted_arguments=(
+ ArgumentSpec("conf", "--conf", "path", "Optional model configuration file."),
+ ArgumentSpec("bootstraps", "--bootstraps", "int", "Bootstrap iterations."),
+ ArgumentSpec("input_excel_protein", "--input-excel-protein", "path", "Protein input CSV file."),
+ ArgumentSpec("input_excel_psite", "--input-excel-psite", "path", "Phosphosite input Excel file."),
+ ArgumentSpec("input_excel_rna", "--input-excel-rna", "path", "RNA input Excel file."),
+ ),
+ required_inputs=("protein CSV", "phosphosite Excel", "RNA Excel"),
+ input_assignments=(
+ InputSpec("config", "TOML config file", "conf", "Optional ProtWise TOML config file.",
+ extensions=(".toml",)),
+ InputSpec("protein_file", "Protein CSV file", "input_excel_protein",
+ "Protein input CSV file read by ProtWise.", extensions=(".csv",)),
+ InputSpec("phosphosite_file", "Phosphosite file", "input_excel_psite", "Phosphosite input Excel file.",
+ extensions=(".xlsx",)),
+ InputSpec("rna_file", "RNA/mRNA file", "input_excel_rna", "RNA input Excel file.", extensions=(".xlsx",)),
+ ),
+ output_dir_arg="--outdir",
+ expected_output_folder="results/protwise-model",
+ result_workflow_keys=("protwise.runner",),
+ ),
+ "networkmodel": WorkflowDescriptor(
+ key="networkmodel",
+ label="Network model",
+ description="Run the integrated global model via networkmodel.runner.",
+ pixi_task="networkmodel",
+ python_module="networkmodel.runner",
+ accepted_arguments=(
+ ArgumentSpec("conf", "--conf", "path", "Optional global model config file."),
+ ArgumentSpec("kinase_net", "--kinase-net", "path", "Kinase network CSV."),
+ ArgumentSpec("tf_net", "--tf-net", "path", "TF network CSV."),
+ ArgumentSpec("ms", "--ms", "path", "Protein/MS data file."),
+ ArgumentSpec("rna", "--rna", "path", "RNA data file."),
+ ArgumentSpec("phospho", "--phospho", "path", "Phosphoproteomics data file."),
+ ArgumentSpec("cores", "--cores", "int", "Worker/core count."),
+ ArgumentSpec("n_gen", "--n-gen", "int", "Maximum optimiser iterations."),
+ ArgumentSpec("seed", "--seed", "int", "Random seed."),
+ ArgumentSpec("scan", "--scan", "bool", "Run hyperparameter scan."),
+ ArgumentSpec("sensitivity", "--sensitivity", "bool", "Run sensitivity analysis."),
+ ArgumentSpec(
+ name="kinopt",
+ flag="--kinopt",
+ ),
+ ArgumentSpec(
+ name="tfopt",
+ flag="--tfopt",
+ ),
+ ),
+ required_inputs=("kinase network", "TF network", "MS/protein data", "RNA data", "phospho data"),
+ input_assignments=(
+ InputSpec("config", "TOML config file", "conf", "Optional networkmodel TOML config file.",
+ extensions=(".toml",)),
+ InputSpec("kinase_network", "Kinase network CSV file", "kinase_net",
+ "Kinase network CSV file read by networkmodel.", extensions=(".csv",)),
+ InputSpec("tf_network", "TF network CSV file", "tf_net", "TF network CSV file read by networkmodel.",
+ extensions=(".csv",)),
+ InputSpec("protein_file", "Protein/MS CSV file", "ms", "Protein/MS CSV data file read by networkmodel.",
+ extensions=(".csv",)),
+ InputSpec("rna_file", "RNA/mRNA CSV file", "rna", "RNA CSV data file read by networkmodel.",
+ extensions=(".csv",)),
+ InputSpec("phosphosite_file", "Phosphoproteomics CSV file", "phospho",
+ "Phosphoproteomics CSV data file read by networkmodel.", extensions=(".csv",)),
+ InputSpec("previous_kinopt", "Previous KinOpt result", "kinopt", "Previous kinopt Excel result.",
+ extensions=(".xlsx",)),
+ InputSpec("previous_tfopt", "Previous TFOpt result", "tfopt", "Previous tfopt Excel result.",
+ extensions=(".xlsx",)),
+ InputSpec("networkmodel_result_dir", "Networkmodel result directory", None,
+ "Reference result directory for browsing; not passed to CLI."),
+ ),
+ output_dir_arg="--output-dir",
+ expected_output_folder="results/networkmodel",
+ result_workflow_keys=("networkmodel.runner",),
+ ),
+ "phoskintime-all": WorkflowDescriptor(
+ key="phoskintime-all",
+ label="PhosKinTime all",
+ description="Run the existing config.cli all wrapper for preprocessing, local TF/KinOpt, and ProtWise model.",
+ pixi_task="phoskintime-all",
+ python_module="config.cli",
+ module_args=("all",),
+ accepted_arguments=(
+ ArgumentSpec("tf_mode", "--tf-mode", "str", "TFOpt mode.", default="local"),
+ ArgumentSpec("kin_mode", "--kin-mode", "str", "KinOpt mode.", default="local"),
+ ArgumentSpec("tf_conf", "--tf-conf", "path", "TFOpt config file."),
+ ArgumentSpec("kin_conf", "--kin-conf", "path", "KinOpt config file."),
+ ArgumentSpec("model_conf", "--model-conf", "path", "ProtWise config file."),
+ ),
+ input_assignments=(
+ InputSpec("tf_config", "TFOpt TOML config file", "tf_conf", "TOML config for TFOpt stage.",
+ extensions=(".toml",)),
+ InputSpec("kin_config", "KinOpt TOML config file", "kin_conf", "TOML config for KinOpt stage.",
+ extensions=(".toml",)),
+ InputSpec("model_config", "ProtWise TOML config file", "model_conf", "TOML config for ProtWise stage.",
+ extensions=(".toml",)),
+ ),
+ output_dir_arg="--outdir",
+ expected_output_folder="results/phoskintime-all",
+ result_workflow_keys=("phoskintime.all",),
+ safe_for_dashboard=True,
+ ),
+}
+
+
+def get_workflow(key: str) -> WorkflowDescriptor:
+ """Return a registered workflow or raise a KeyError with a helpful message."""
+ try:
+ return WORKFLOWS[key]
+ except KeyError as exc:
+ known = ", ".join(sorted(WORKFLOWS))
+ raise KeyError(f"Unknown workflow {key!r}. Known workflows: {known}") from exc
+
+
+def launchable_workflows() -> list[WorkflowDescriptor]:
+ """Return dashboard-safe workflows that have an executable Python module."""
+ return [wf for wf in sorted(WORKFLOWS.values(), key=lambda item: item.key) if
+ wf.safe_for_dashboard and wf.python_module]
+
+
+def infer_workflow(inventory: ResultInventory) -> WorkflowDescriptor:
+ """Infer workflow identity from metadata first, then legacy filenames."""
+ if inventory.metadata is not None:
+ try:
+ metadata = json.loads(inventory.metadata.read_text(encoding="utf-8"))
+ workflow = str(metadata.get("workflow", "")).strip()
+ for descriptor in WORKFLOWS.values():
+ if workflow == descriptor.key or workflow in descriptor.result_workflow_keys:
+ return descriptor
+ except (OSError, json.JSONDecodeError):
+ pass
+
+ names = {item.name for item in inventory.tables}
+ if "kinopt_results.xlsx" in names:
+ return WORKFLOWS["kinopt.local"]
+ if "tfopt_results.xlsx" in names:
+ return WORKFLOWS["tfopt.local"]
+ if {"scalar_objective.csv", "pred_prot_picked.csv", "pred_rna_picked.csv", "pred_phospho_picked.csv"} & names:
+ return WORKFLOWS["networkmodel.runner"]
+ return WORKFLOWS["unknown"]
+
+
+def registered_workflows() -> list[WorkflowDescriptor]:
+ """Return all known workflow descriptors for display or tests."""
+ return [WORKFLOWS[key] for key in sorted(WORKFLOWS)]
+
+
+def pixi_environments(pixi_toml: str | Path = "pixi.toml") -> list[str]:
+ """Return Pixi environments defined by pixi.toml, always including default first."""
+ path = Path(pixi_toml)
+ envs = ["default"]
+ if not path.is_file():
+ return envs
+ try:
+ import tomllib
+ with path.open("rb") as handle:
+ data = tomllib.load(handle)
+ configured = data.get("environments", {}) or {}
+ for name in configured:
+ if name not in envs:
+ envs.append(str(name))
+ except Exception:
+ return envs
+ return envs
diff --git a/dashboard/result_parser.py b/dashboard/result_parser.py
new file mode 100644
index 0000000..29f7826
--- /dev/null
+++ b/dashboard/result_parser.py
@@ -0,0 +1,179 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+
+from dashboard.file_utils import (
+ DisplayFile,
+ PLOT_EXTENSIONS,
+ REPORT_EXTENSIONS,
+ TABLE_EXTENSIONS,
+ filter_by_suffix,
+ iter_files,
+ resolve_directory,
+)
+
+LEGACY_TABLE_NAMES = {
+ "scalar_objective.csv",
+ "convergence_history.csv",
+ "pred_prot_picked.csv",
+ "pred_rna_picked.csv",
+ "pred_phospho_picked.csv",
+ "kinopt_results.xlsx",
+ "tfopt_results.xlsx",
+}
+LEGACY_ANALYSIS_DIRS = ("optimization", "profiles", "posterior", "plots")
+PROVENANCE_FILES = ("metadata.json", "command.txt", "console.log", "config_resolved.yaml")
+STANDARD_SUBDIRS = {"tables", "plots", "logs", "reports", "artifacts"}
+CHILD_WORKFLOW_DIRS = {"tfopt", "kinopt", "protwise", "networkmodel"}
+
+
+@dataclass(frozen=True)
+class ResultInventory:
+ """Lazy inventory of files in a PhosKinTime result directory."""
+
+ root: Path
+ metadata: Path | None = None
+ command: Path | None = None
+ console_log: Path | None = None
+ config: Path | None = None
+ tables: list[DisplayFile] = field(default_factory=list)
+ plots: list[DisplayFile] = field(default_factory=list)
+ logs: list[DisplayFile] = field(default_factory=list)
+ reports: list[DisplayFile] = field(default_factory=list)
+ artifacts: list[DisplayFile] = field(default_factory=list)
+ child_runs: list[Path] = field(default_factory=list)
+ missing_expected: list[str] = field(default_factory=list)
+
+ @property
+ def has_content(self) -> bool:
+ return any(
+ (
+ self.metadata,
+ self.command,
+ self.console_log,
+ self.config,
+ self.tables,
+ self.plots,
+ self.logs,
+ self.reports,
+ self.artifacts,
+ self.child_runs,
+ )
+ )
+
+
+def _display_files(root: Path, paths: list[Path], category: str) -> list[DisplayFile]:
+ seen: set[Path] = set()
+ files: list[DisplayFile] = []
+ for path in paths:
+ resolved = path.resolve()
+ if resolved in seen:
+ continue
+ seen.add(resolved)
+ files.append(DisplayFile(path=path, root=root, category=category))
+ return files
+
+
+def _is_report_like_html(path: Path) -> bool:
+ """Return whether an HTML file should be treated as a report rather than a plot."""
+ return path.suffix.lower() in {".html", ".htm"} and "report" in path.stem.lower()
+
+
+def _is_child_result_dir(path: Path) -> bool:
+ """Detect immediate nested workflow outputs such as phoskintime-all stage folders."""
+ if not path.is_dir() or path.name in STANDARD_SUBDIRS:
+ return False
+ if path.name in CHILD_WORKFLOW_DIRS:
+ return True
+ if any((path / name).is_file() for name in PROVENANCE_FILES):
+ return True
+ if any((path / dirname).is_dir() for dirname in STANDARD_SUBDIRS):
+ return True
+ if any((path / name).is_file() for name in LEGACY_TABLE_NAMES):
+ return True
+ if any((path / dirname).is_dir() for dirname in LEGACY_ANALYSIS_DIRS):
+ return True
+ return False
+
+
+def discover_child_result_directories(path: str | Path) -> list[Path]:
+ """Return immediate nested result directories without recursively parsing contents."""
+ root = resolve_directory(path)
+ return sorted(
+ (child.resolve() for child in root.iterdir() if _is_child_result_dir(child)),
+ key=lambda p: p.name.lower(),
+ )
+
+
+def _top_level_legacy_tables(root: Path) -> list[Path]:
+ return sorted((root / name for name in LEGACY_TABLE_NAMES if (root / name).is_file()), key=lambda p: p.name.lower())
+
+
+def _legacy_dir_files(root: Path, suffixes: set[str]) -> list[Path]:
+ files: list[Path] = []
+ for dirname in LEGACY_ANALYSIS_DIRS:
+ files.extend(filter_by_suffix(iter_files(root / dirname, recursive=True), suffixes))
+ return files
+
+
+def discover_result_directory(path: str | Path) -> ResultInventory:
+ """Discover dashboard-readable files in a result directory without loading file contents."""
+ root = resolve_directory(path)
+
+ metadata = root / "metadata.json" if (root / "metadata.json").is_file() else None
+ command = root / "command.txt" if (root / "command.txt").is_file() else None
+ console_log = root / "console.log" if (root / "console.log").is_file() else None
+ config = root / "config_resolved.yaml" if (root / "config_resolved.yaml").is_file() else None
+
+ table_paths = filter_by_suffix(iter_files(root / "tables"), TABLE_EXTENSIONS)
+ table_paths.extend(_top_level_legacy_tables(root))
+ table_paths.extend(_legacy_dir_files(root, TABLE_EXTENSIONS))
+
+ top_level_files = [p for p in root.iterdir() if p.is_file()]
+ plot_dir_paths = filter_by_suffix(iter_files(root / "plots"), PLOT_EXTENSIONS)
+ legacy_plot_paths = _legacy_dir_files(root, PLOT_EXTENSIONS)
+ report_like_html = [
+ p
+ for p in [*top_level_files, *plot_dir_paths, *legacy_plot_paths]
+ if _is_report_like_html(p)
+ ]
+
+ plot_paths = [p for p in plot_dir_paths if not _is_report_like_html(p)]
+ plot_paths.extend(p for p in legacy_plot_paths if not _is_report_like_html(p))
+ plot_paths.extend(
+ filter_by_suffix(
+ (p for p in top_level_files if not _is_report_like_html(p)),
+ PLOT_EXTENSIONS,
+ )
+ )
+
+ log_paths = iter_files(root / "logs")
+ if console_log is not None:
+ log_paths.insert(0, console_log)
+
+ report_paths = filter_by_suffix(iter_files(root / "reports"), REPORT_EXTENSIONS)
+ report_paths.extend(filter_by_suffix(report_like_html, REPORT_EXTENSIONS))
+
+ artifact_paths = iter_files(root / "artifacts")
+ child_runs = discover_child_result_directories(root)
+
+ missing = [name for name in PROVENANCE_FILES if not (root / name).is_file()]
+ for dirname in ("tables", "plots", "logs", "reports", "artifacts"):
+ if not (root / dirname).is_dir():
+ missing.append(f"{dirname}/")
+
+ return ResultInventory(
+ root=root,
+ metadata=metadata,
+ command=command,
+ console_log=console_log,
+ config=config,
+ tables=_display_files(root, table_paths, "table"),
+ plots=_display_files(root, plot_paths, "plot"),
+ logs=_display_files(root, log_paths, "log"),
+ reports=_display_files(root, report_paths, "report"),
+ artifacts=_display_files(root, artifact_paths, "artifact"),
+ child_runs=child_runs,
+ missing_expected=missing,
+ )
diff --git a/dashboard/runner.py b/dashboard/runner.py
new file mode 100644
index 0000000..c6b902a
--- /dev/null
+++ b/dashboard/runner.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import json
+import subprocess
+from collections.abc import Callable, Iterator
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Literal
+
+from common.results import ensure_result_dir, write_command, write_metadata
+from dashboard.command_builder import BuiltCommand
+
+RunStatus = Literal["running", "success", "failure", "cancelled"]
+
+
+@dataclass(frozen=True)
+class RunEvent:
+ """A single launcher event for the dashboard console/status panels."""
+
+ status: RunStatus
+ line: str = ""
+ returncode: int | None = None
+ outdir: Path | None = None
+
+
+def _merge_metadata(outdir: Path, updates: dict) -> None:
+ path = outdir / "metadata.json"
+ try:
+ existing = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {}
+ except json.JSONDecodeError:
+ existing = {}
+ existing.update(updates)
+ path.write_text(json.dumps(existing, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+
+
+def prepare_run_provenance(built: BuiltCommand, extra: dict | None = None) -> Path:
+ """Create result folders and initial launcher provenance before subprocess execution."""
+ outdir = ensure_result_dir(built.outdir)["root"]
+ write_command(outdir, built.command)
+ write_metadata(
+ outdir,
+ workflow=built.workflow.result_workflow_keys[0] if built.workflow.result_workflow_keys else built.workflow.key,
+ args={
+ "launcher_workflow": built.workflow.key,
+ "pixi_environment": built.pixi_environment,
+ "command": built.command,
+ },
+ extra={"launcher": "dashboard", "launcher_status": "running", **(extra or {})},
+ )
+ return outdir
+
+
+def stream_command(
+ command: list[str],
+ *,
+ cwd: str | Path,
+ outdir: str | Path,
+ env: dict[str, str] | None = None,
+ cancel_check: Callable[[], bool] | None = None,
+) -> Iterator[RunEvent]:
+ """Run a command with shell=False, streaming merged stdout/stderr and writing console.log."""
+ root = ensure_result_dir(outdir)["root"]
+ console_path = root / "console.log"
+ with console_path.open("a", encoding="utf-8") as console:
+ console.write(f"\n[dashboard] start {datetime.now(timezone.utc).isoformat()}\n")
+ console.write("[dashboard] command " + " ".join(command) + "\n")
+ process = subprocess.Popen(
+ command,
+ cwd=Path(cwd),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ shell=False,
+ env=env,
+ )
+ cancelled = False
+ assert process.stdout is not None
+ for line in process.stdout:
+ console.write(line)
+ console.flush()
+ yield RunEvent(status="running", line=line, outdir=root)
+ if cancel_check is not None and cancel_check():
+ cancelled = True
+ process.terminate()
+ break
+ returncode = process.wait()
+ if cancelled:
+ status: RunStatus = "cancelled"
+ else:
+ status = "success" if returncode == 0 else "failure"
+ console.write(f"[dashboard] {status} returncode={returncode}\n")
+ console.flush()
+ yield RunEvent(status=status, returncode=returncode, outdir=root)
+
+
+def run_built_command(
+ built: BuiltCommand,
+ *,
+ repo_root: str | Path,
+ cancel_check: Callable[[], bool] | None = None,
+) -> Iterator[RunEvent]:
+ """Prepare provenance, execute a built workflow command, and update run metadata."""
+ outdir = prepare_run_provenance(built)
+ final_event: RunEvent | None = None
+ for event in stream_command(built.command, cwd=repo_root, outdir=outdir, cancel_check=cancel_check):
+ final_event = event
+ yield event
+ if final_event is not None and final_event.status in {"success", "failure", "cancelled"}:
+ _merge_metadata(
+ outdir,
+ {
+ "launcher_status": final_event.status,
+ "launcher_returncode": final_event.returncode,
+ "launcher_completed_at": datetime.now(timezone.utc).isoformat(),
+ },
+ )
+
+
+def log_tail(outdir: str | Path, lines: int = 40) -> str:
+ """Return a bounded console.log tail for failure display."""
+ path = Path(outdir) / "console.log"
+ if not path.is_file():
+ return ""
+ return "".join(path.read_text(encoding="utf-8", errors="replace").splitlines(keepends=True)[-lines:])
diff --git a/dashboard/workflow_panels/__init__.py b/dashboard/workflow_panels/__init__.py
new file mode 100644
index 0000000..2968679
--- /dev/null
+++ b/dashboard/workflow_panels/__init__.py
@@ -0,0 +1 @@
+"""Workflow-specific dashboard panels."""
diff --git a/dashboard/workflow_panels/analysis.py b/dashboard/workflow_panels/analysis.py
new file mode 100644
index 0000000..0d70ecd
--- /dev/null
+++ b/dashboard/workflow_panels/analysis.py
@@ -0,0 +1,138 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+from common.results import ensure_result_dir
+
+
+@dataclass(frozen=True)
+class AnalysisTask:
+ key: str
+ label: str
+ description: str
+ script: str
+ arguments: dict[str, str] = field(default_factory=dict)
+
+
+ANALYSIS_TASKS: dict[str, AnalysisTask] = {
+ "tf-kin-counts": AnalysisTask(
+ "tf-kin-counts",
+ "TF/Kin counts",
+ "Summarise TF and kinase psite/target counts from TFOpt and KinOpt workbooks.",
+ "scripts/analyze_tf_kin_counts.py",
+ {"tfopt_xlsx": "--tfopt-xlsx", "kinopt_xlsx": "--kinopt-xlsx", "out_dir": "--out-dir"},
+ ),
+ "curve-similarity": AnalysisTask(
+ "curve-similarity",
+ "Curve similarity / Fréchet distance",
+ "Run the existing curve similarity backend for TFOpt and KinOpt workbooks.",
+ "scripts/curve_similarity.py",
+ {"tfopt_xlsx": "--tfopt-xlsx", "kinopt_xlsx": "--kinopt-xlsx", "out_dir": "--out-dir"},
+ ),
+ "export-subnetworks": AnalysisTask(
+ "export-subnetworks",
+ "Export subnetworks",
+ "Run the existing subnetworks export script on kinase and TF network input files.",
+ "scripts/export_subnetworks.py",
+ {"input2": "--input2", "input4": "--input4", "out_dir": "--outdir", "hops": "--hops"},
+ ),
+ "protein-accumulators": AnalysisTask(
+ "protein-accumulators",
+ "Protein accumulators",
+ "Detect protein-vs-RNA accumulator patterns from networkmodel prediction CSVs.",
+ "scripts/find_protein_accumulators.py",
+ {"prot": "--prot", "rna": "--rna", "threshold": "--threshold"},
+ ),
+ "mechanistic-insights": AnalysisTask(
+ "mechanistic-insights",
+ "Mechanistic insights",
+ "Run the existing mechanistic insights script; inputs are passed through its CLI.",
+ "scripts/mechanistic_insights.py",
+ {
+ "kinase_net": "--kinase-net",
+ "tf_net": "--tf-net",
+ "ms": "--ms",
+ "rna": "--rna",
+ "phospho": "--phospho",
+ "kinopt": "--kinopt",
+ "tfopt": "--tfopt",
+ "output_dir": "--output-dir",
+ "cores": "--cores",
+ },
+ ),
+ "temporal-sensitivity": AnalysisTask(
+ "temporal-sensitivity",
+ "Temporal sensitivity",
+ "Run global temporal sensitivity analysis for an existing networkmodel results directory.",
+ "scripts/temporal_sensitivity.py",
+ {"results_dir": "--results-dir", "samples": "--samples"},
+ ),
+}
+
+
+def analysis_output_dir(result_dir: str | Path, task_key: str) -> Path:
+ """Create a dashboard-standard artifacts/reports area for analysis outputs."""
+ paths = ensure_result_dir(result_dir)
+ out_dir = paths["artifacts"] / "analysis" / task_key
+ out_dir.mkdir(parents=True, exist_ok=True)
+ return out_dir
+
+
+def build_analysis_command(task_key: str, values: dict[str, Any], result_dir: str | Path) -> list[str]:
+ """Build a safe argv-list command for an existing analysis script."""
+ if task_key not in ANALYSIS_TASKS:
+ raise KeyError(f"Unknown analysis task: {task_key}")
+ task = ANALYSIS_TASKS[task_key]
+ command = ["python", task.script]
+ merged = dict(values)
+ if "out_dir" in task.arguments and not merged.get("out_dir"):
+ merged["out_dir"] = analysis_output_dir(result_dir, task_key)
+ elif task_key == "mechanistic-insights" and not merged.get("output_dir"):
+ merged["output_dir"] = result_dir
+ elif task_key in {"protein-accumulators", "temporal-sensitivity"}:
+ analysis_output_dir(result_dir, task_key)
+ for name, flag in task.arguments.items():
+ value = merged.get(name)
+ if value is None or value == "":
+ continue
+ command.extend([flag, str(value)])
+ return command
+
+
+def discover_analysis_outputs(result_dir: str | Path) -> dict[str, list[Path]]:
+ """Discover outputs previously written by dashboard-triggered analyses."""
+ root = Path(result_dir)
+ base = root / "artifacts" / "analysis"
+ if not base.is_dir():
+ return {}
+ return {
+ task_dir.name: sorted(path for path in task_dir.rglob("*") if path.is_file())
+ for task_dir in sorted(base.iterdir())
+ if task_dir.is_dir()
+ }
+
+
+def render(root: str | Path) -> None:
+ import streamlit as st
+
+ st.subheader("Advanced analyses")
+ st.caption(
+ "Analyses are not run automatically. Build a command, review it, and run via the launcher or terminal when appropriate.")
+ task = st.selectbox("Analysis", list(ANALYSIS_TASKS.values()), format_func=lambda item: item.label)
+ st.write(task.description)
+ values: dict[str, Any] = {}
+ for name in task.arguments:
+ if name == "out_dir":
+ continue
+ values[name] = st.text_input(name.replace("_", " "), value="")
+ try:
+ command = build_analysis_command(task.key, values, root)
+ st.code(" ".join(command), language="bash")
+ except Exception as exc:
+ st.error(str(exc))
+ outputs = discover_analysis_outputs(root)
+ if outputs:
+ st.write("Existing analysis outputs")
+ st.json({key: [str(path) for path in paths] for key, paths in outputs.items()})
diff --git a/dashboard/workflow_panels/common.py b/dashboard/workflow_panels/common.py
new file mode 100644
index 0000000..95249be
--- /dev/null
+++ b/dashboard/workflow_panels/common.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+
+from dashboard.file_utils import DisplayFile
+from dashboard.result_parser import discover_result_directory
+
+
+@dataclass(frozen=True)
+class WorkflowPanelData:
+ root: Path
+ primary_result: Path | None = None
+ tables: dict[str, Path] = field(default_factory=dict)
+ plots: list[DisplayFile] = field(default_factory=list)
+ reports: list[DisplayFile] = field(default_factory=list)
+ artifacts: list[DisplayFile] = field(default_factory=list)
+ messages: list[str] = field(default_factory=list)
+
+ @property
+ def has_content(self) -> bool:
+ return bool(self.primary_result or self.tables or self.plots or self.reports or self.artifacts)
+
+
+def _first_existing(root: Path, names: tuple[str, ...]) -> Path | None:
+ candidates = []
+ for name in names:
+ candidates.extend([root / name, root / "tables" / name, root / "artifacts" / name])
+ return next((path for path in candidates if path.is_file()), None)
+
+
+def _inventory(root: str | Path):
+ return discover_result_directory(root)
diff --git a/dashboard/workflow_panels/kinopt.py b/dashboard/workflow_panels/kinopt.py
new file mode 100644
index 0000000..7338613
--- /dev/null
+++ b/dashboard/workflow_panels/kinopt.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.workflow_panels.common import WorkflowPanelData, _first_existing, _inventory
+
+KINOPT_SHEETS = ("Alpha Values", "Beta Values", "Observed", "Estimated", "Residuals", "Summary")
+
+
+def discover_kinopt_panel(root: str | Path) -> WorkflowPanelData:
+ """Discover KinOpt result files and generated displays."""
+ root = Path(root).resolve()
+ inventory = _inventory(root)
+ result = _first_existing(root, ("kinopt_results.xlsx",))
+ tables = {"kinopt_results": result} if result else {}
+ messages = [] if result else ["No kinopt_results.xlsx file found in the result directory or tables/."]
+ return WorkflowPanelData(root=root, primary_result=result, tables=tables, plots=inventory.plots,
+ reports=inventory.reports, artifacts=inventory.artifacts, messages=messages)
+
+
+def render(root: str | Path) -> None:
+ """Render KinOpt-specific outputs without recomputing analyses."""
+ import pandas as pd
+ import streamlit as st
+
+ data = discover_kinopt_panel(root)
+ st.subheader("KinOpt panel")
+ for message in data.messages:
+ st.info(message)
+ if data.primary_result:
+ st.caption(f"Workbook: `{data.primary_result}`")
+ for sheet in KINOPT_SHEETS:
+ try:
+ df = pd.read_excel(data.primary_result, sheet_name=sheet)
+ except Exception:
+ continue
+ with st.expander(sheet, expanded=sheet in {"Alpha Values", "Beta Values"}):
+ st.dataframe(df, use_container_width=True)
+ _render_existing_media(data)
+
+
+def _render_existing_media(data: WorkflowPanelData) -> None:
+ import streamlit as st
+
+ if data.plots:
+ st.write("Existing plots")
+ for item in data.plots[:12]:
+ if item.suffix in {".png", ".jpg", ".jpeg", ".svg"}:
+ st.image(str(item.path), caption=item.relative_path, use_container_width=True)
+ else:
+ st.write(item.relative_path)
+ if data.reports:
+ st.write("Reports")
+ for item in data.reports:
+ st.write(item.relative_path)
diff --git a/dashboard/workflow_panels/networkmodel.py b/dashboard/workflow_panels/networkmodel.py
new file mode 100644
index 0000000..df63ae9
--- /dev/null
+++ b/dashboard/workflow_panels/networkmodel.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.workflow_panels.common import WorkflowPanelData, _first_existing, _inventory
+
+NETWORK_TABLES = (
+ "scalar_objective.csv",
+ "convergence_history.csv",
+ "pred_prot_picked.csv",
+ "pred_rna_picked.csv",
+ "pred_phospho_picked.csv",
+ "picked_objectives.json",
+)
+
+
+def discover_networkmodel_panel(root: str | Path) -> WorkflowPanelData:
+ """Discover networkmodel bundle, scalar objective, predictions, and inference outputs."""
+ root = Path(root).resolve()
+ inventory = _inventory(root)
+ bundle = _first_existing(root, ("dashboard_bundle.pkl", "networkmodel_dashboard_bundle.pkl"))
+ tables = {}
+ for name in NETWORK_TABLES:
+ found = _first_existing(root, (name,))
+ if found:
+ tables[name] = found
+ for item in inventory.tables:
+ if item.relative_path.startswith(("optimization/", "profiles/", "posterior/")):
+ tables[item.relative_path] = item.path
+ messages = [] if (bundle or tables or inventory.plots) else [
+ "No networkmodel bundle, scalar objective, predictions, or inference outputs were found."]
+ return WorkflowPanelData(root=root, primary_result=bundle, tables=tables, plots=inventory.plots,
+ reports=inventory.reports, artifacts=inventory.artifacts, messages=messages)
+
+
+def render(root: str | Path) -> None:
+ import pandas as pd
+ import streamlit as st
+
+ data = discover_networkmodel_panel(root)
+ st.subheader("Networkmodel panel")
+ for message in data.messages:
+ st.info(message)
+ if data.primary_result:
+ st.success(f"Dashboard bundle found: {data.primary_result}")
+ try:
+ from networkmodel.dashboard_bundle import load_dashboard_bundle
+ bundle = load_dashboard_bundle(data.root)
+ st.json({"bundle_keys": sorted(bundle.keys())})
+ except Exception as exc:
+ st.warning(f"Bundle exists but could not be loaded here: {exc}")
+ for label, path in data.tables.items():
+ with st.expander(label, expanded=label == "scalar_objective.csv"):
+ try:
+ if path.suffix == ".csv":
+ st.dataframe(pd.read_csv(path), use_container_width=True)
+ elif path.suffix == ".json":
+ st.json(__import__("json").loads(path.read_text(encoding="utf-8")))
+ except Exception as exc:
+ st.warning(f"Could not read {label}: {exc}")
+ if data.plots:
+ st.write("Networkmodel plots")
+ for item in data.plots[:12]:
+ if item.suffix in {".png", ".jpg", ".jpeg", ".svg"}:
+ st.image(str(item.path), caption=item.relative_path, use_container_width=True)
+ else:
+ st.write(item.relative_path)
diff --git a/dashboard/workflow_panels/protwise.py b/dashboard/workflow_panels/protwise.py
new file mode 100644
index 0000000..db93dda
--- /dev/null
+++ b/dashboard/workflow_panels/protwise.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.workflow_panels.common import WorkflowPanelData, _inventory
+
+PROTWISE_KEYWORDS = ("fit", "residual", "sensitivity", "prediction", "model_error", "regularization")
+
+
+def discover_protwise_panel(root: str | Path) -> WorkflowPanelData:
+ """Discover ProtWise tables, plots, sensitivity outputs, and reports."""
+ root = Path(root).resolve()
+ inventory = _inventory(root)
+ tables = {
+ item.relative_path: item.path
+ for item in inventory.tables
+ if any(key in item.name.lower() for key in PROTWISE_KEYWORDS) or item.suffix in {".xlsx", ".csv"}
+ }
+ plots = [item for item in inventory.plots if
+ any(key in item.name.lower() for key in PROTWISE_KEYWORDS)] or inventory.plots
+ messages = [] if (tables or plots or inventory.reports) else [
+ "No ProtWise-specific outputs were discovered; run the model first or select another result directory."]
+ return WorkflowPanelData(root=root, tables=tables, plots=plots, reports=inventory.reports,
+ artifacts=inventory.artifacts, messages=messages)
+
+
+def render(root: str | Path) -> None:
+ import pandas as pd
+ import streamlit as st
+
+ data = discover_protwise_panel(root)
+ st.subheader("ProtWise panel")
+ for message in data.messages:
+ st.info(message)
+ for label, path in data.tables.items():
+ with st.expander(label):
+ try:
+ if path.suffix == ".csv":
+ st.dataframe(pd.read_csv(path), use_container_width=True)
+ elif path.suffix in {".xlsx", ".xls"}:
+ st.dataframe(pd.read_excel(path), use_container_width=True)
+ except Exception as exc:
+ st.warning(f"Could not read {label}: {exc}")
+ for item in data.plots[:12]:
+ if item.suffix in {".png", ".jpg", ".jpeg", ".svg"}:
+ st.image(str(item.path), caption=item.relative_path, use_container_width=True)
+ else:
+ st.write(item.relative_path)
diff --git a/dashboard/workflow_panels/tfopt.py b/dashboard/workflow_panels/tfopt.py
new file mode 100644
index 0000000..7a1c00a
--- /dev/null
+++ b/dashboard/workflow_panels/tfopt.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from dashboard.workflow_panels.common import WorkflowPanelData, _first_existing, _inventory
+
+TFOPT_SHEETS = ("Alpha Values", "Beta Values", "Observed", "Estimated", "Residuals", "Optimization Results")
+
+
+def discover_tfopt_panel(root: str | Path) -> WorkflowPanelData:
+ """Discover TFOpt result files and generated displays."""
+ root = Path(root).resolve()
+ inventory = _inventory(root)
+ result = _first_existing(root, ("tfopt_results.xlsx",))
+ tables = {"tfopt_results": result} if result else {}
+ messages = [] if result else ["No tfopt_results.xlsx file found in the result directory or tables/."]
+ return WorkflowPanelData(root=root, primary_result=result, tables=tables, plots=inventory.plots,
+ reports=inventory.reports, artifacts=inventory.artifacts, messages=messages)
+
+
+def render(root: str | Path) -> None:
+ """Render TFOpt-specific outputs without recomputing analyses."""
+ import pandas as pd
+ import streamlit as st
+
+ data = discover_tfopt_panel(root)
+ st.subheader("TFOpt panel")
+ for message in data.messages:
+ st.info(message)
+ if data.primary_result:
+ st.caption(f"Workbook: `{data.primary_result}`")
+ for sheet in TFOPT_SHEETS:
+ try:
+ df = pd.read_excel(data.primary_result, sheet_name=sheet)
+ except Exception:
+ continue
+ with st.expander(sheet, expanded=sheet in {"Alpha Values", "Beta Values"}):
+ st.dataframe(df, use_container_width=True)
+ if data.plots:
+ st.write("Existing plots and latent/dominance/knockout exports")
+ for item in data.plots[:12]:
+ if item.suffix in {".png", ".jpg", ".jpeg", ".svg"}:
+ st.image(str(item.path), caption=item.relative_path, use_container_width=True)
+ else:
+ st.write(item.relative_path)
diff --git a/docs/Combinatorial_Model_Memory_Issue.md b/docs/Combinatorial_Model_Memory_Issue.md
new file mode 100644
index 0000000..3d7a3d5
--- /dev/null
+++ b/docs/Combinatorial_Model_Memory_Issue.md
@@ -0,0 +1,302 @@
+# Memory Audit and Safe Fix Plan for Combinatorial Model OOM in phoskintime
+
+## 1. Executive Summary
+
+The `phoskintime` repository implements three network‑based models of phosphorylation: distributive (MODEL=0), sequential (MODEL=1) and combinatorial (MODEL=2). The first two models run within the available memory, but the combinatorial model exhausts available RAM and crashes the server. In a reported run the server had ~503 GB of RAM and `network.runner` alone consumed ~173 GB before failing. This audit inspects the `global` branch and pinpoints specific code paths where the combinatorial model materializes extremely large arrays and keeps them in memory. The planned fixes are conservative: they reduce memory amplification without changing the underlying biological equations or optimization objectives. They focus on streaming, chunking and down‑casting data instead of fully materializing combinatorial state spaces and trajectories, and on avoiding retention of large intermediate results. Only memory‑safe fixes are proposed; no changes to model semantics, optimization logic or public APIs are allowed.
+
+## 2. Observed Failure
+
+1. **Symptom** – The combinatorial model (MODEL=2) causes the server to run out of memory. During a multi‑start optimization the process consumes ~173 GB of RAM (much higher than the distributive/sequential models) and eventually crashes.
+2. **Scale** – Memory growth appears to scale exponentially with the number of phosphorylation sites per protein: the state dimension for each protein is `1 + 2^n_sites`[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,i), and the simulation stores the entire trajectory `Y` of shape `(time_points, total_state_dim)`[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/simulate.py#:~:text=ns%20%3D%20int%28idx.n_states,p0%20%3D%20st%20%2B%201).
+3. **Immediate cause** – The combinatorial model precomputes all possible phosphorylation states and transitions for every protein using dense arrays. During simulation it materializes these states for all time points and computes per‑site signals using a dense bit‑mask matrix. These arrays can easily reach hundreds of gigabytes when proteins have more than a handful of phosphosites.
+
+## 3. Root‑Cause Hypotheses Checked
+
+| Hypothesis | Evidence | Outcome |
+|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
+| **3.1 Large state dimension due to 2^n_sites** | The `Index` class sets `n_states[i] = 1 << n_sites[i]` for combinatorial models, so each protein has `1 + 2^n_sites` dynamic states[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,i). | Confirmed. This exponential growth is inherent to the model. |
+| **3.2 Dense hypercube transitions stored in memory** | `build_random_transitions` enumerates all dephosphorylation/phosphorylation transitions for each state and appends them to `trans_from`, `trans_to` and `trans_site` arrays[\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/models.py#:~:text=def%20build_random_transitions%28idx%29%3A%20,for%20combinatorial%20topology). These arrays scale with `2^n_sites × n_sites`. | Confirmed. For `n_sites=6` this yields ~384 transitions per protein; for `n_sites=10` it becomes ~10,240 transitions. |
+| **3.3 Dense** `bits` **matrix used to extract signals** | During simulation output extraction, the code constructs `bits = ((np.arange(ns)[:,None] >> np.arange(n_sites)[None,:]) & 1).astype(float)` and multiplies the state trajectories with this `(2^n_sites × n_sites)` matrix to compute per‑site phosphorylation counts[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/simulate.py#:~:text=ns%20%3D%20int%28idx.n_states,p0%20%3D%20st%20%2B%201). | Confirmed. The `bits` matrix is dense and duplicates weight vectors for each site. |
+| **3.4 Full trajectory materialization for all proteins and multi‑starts** | `simulate_diffrax` returns the entire trajectory `Y` for all dynamic states and time points. In `simulate_and_measure`, the code slices `Y` to extract per‑protein states and does not free them. Sensitivity analysis and multi‑start keep DataFrames of all trajectories and metrics in memory. | Confirmed. These arrays are not released before the next iteration. |
+| **3.5 JAX backend uses global max array sizes** | The JAX RHS constructs arrays with `jnp.arange(max_states)` and loops over `max_trans` for all proteins[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/backend.py#:~:text=). `max_states` is the maximum 2^n_sites across proteins. | Confirmed. On GPUs this increases host memory and JAX compilation time. |
+| **3.6** `S_cache` **allocated densely for all phosphorylation sites** | In combinatorial mode, `System.__init__` allocates `S_cache = np.zeros((n_W_rows, kin_Kmat.shape[1]))` where `n_W_rows` equals the total number of phosphosites across all proteins[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,1%5D%29%2C%20dtype%3Dnp.float64). If there are many sites or time bins this array is large. | Partially confirmed – moderate memory, but not the main issue compared with exponential state/transition explosion. |
+| **3.7 Retention of multistart/sensitivity history** | Multi‑start results and sensitivity analyses accumulate large DataFrames containing trajectories, parameter sets and metrics. These are not cleared until all starts finish. | Confirmed. This retention amplifies memory usage but is secondary to the exponential state/transition costs. |
+
+## 4. Confirmed Memory‑Amplification Points
+
+1. **Exponential state space** – `Index.__init__` computes `self.n_states[i] = 1 << n_sites[i]`[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,i). Each protein’s ODE segment includes `1 + 2^n_sites` states, leading to a `state_dim` that grows exponentially with `n_sites`. Running several proteins with more than 6 sites saturates memory quickly.
+
+2. **Dense transition lists** – `build_random_transitions` iterates over all possible states `m` (`0..2^n_sites-1`) and for each site `j` appends `m`, `m|(1<> np.arange(n_sites)[None,:]) & 1).astype(float)` and performing `states @ bits`[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/simulate.py#:~:text=ns%20%3D%20int%28idx.n_states,p0%20%3D%20st%20%2B%201). For `ns = 2^n_sites`, `bits` is a float matrix of size `ns × n_sites`. When `ns` is large this matrix alone can occupy tens of gigabytes.
+
+4. **Full trajectory retention** – `simulate_diffrax` returns the complete trajectory `Y` across all time points. `simulate_and_measure` slices `Y` and constructs DataFrames for each protein, keeping the full arrays alive until the end of optimization or sensitivity analysis. Multi‑start and sensitivity analysis store these DataFrames in lists, multiplying memory usage by the number of starts or samples.
+
+5. **Global JAX array sizes** – In the JAX backend, `make_networkmodel_rhs` constructs arrays of size `max_states` and `max_trans` across all proteins[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/backend.py#:~:text=). This amplifies memory usage when even one protein has a large number of states.
+
+6. `S_cache` **zero‑initialization** – For combinatorial models, `System.__init__` allocates `S_cache` as a dense zero array sized `(number_of_sites, number_of_time_bins)`[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,1%5D%29%2C%20dtype%3Dnp.float64). Although smaller than the state trajectory, it is unnecessary to pre‑allocate the entire matrix when only a few columns are needed at a time.
+
+## 5. Files and Exact Lines to Change
+
+The following modifications reduce memory usage without altering the scientific equations or optimization logic. Line numbers refer to the `global` branch as inspected. Only the listed changes should be made; other code must remain unchanged.
+
+### File: `networkmodel/models.py`
+
+Current lines:
+
+- **L454‑498** – Function `build_random_transitions` enumerates all forward transitions for combinatorial models. It loops over each state `m` (`0..2^n-1`) and for each site `j` not set in `m`, appends `m` (from state), `m|(1<> np.arange(n_sites)[None,:]) & 1).astype(float)`; then computes `states @ bits` to get per‑site phosphorylated counts[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/simulate.py#:~:text=ns%20%3D%20int%28idx.n_states,p0%20%3D%20st%20%2B%201).
+
+Required change:
+
+- **Replace** the dense `bits` matrix with per‑site streaming. For each site `j` (0..n_sites‑1), compute a weight vector `w_j = ((np.arange(ns) >> j) & 1).astype(np.float32)` on‑the‑fly and calculate `states @ w_j` for that site. Or compute contributions sequentially within the loss function without constructing `w_j` at all, by using bitwise operations in the loop over states. Accumulate results into a `(times,)` vector for each site. This avoids allocating the `(ns × n_sites)` matrix and reduces memory by a factor of `n_sites`. Since each `w_j` is computed identically to a column of `bits`, the numerical outputs remain unchanged.
+
+- **Free** the sliced `states` array as soon as per‑site and total protein signals are computed. Convert per‑site results to `float32` before storing in DataFrames. Do not keep `states` or intermediate arrays beyond the extraction step.
+
+### File: `networkmodel/backend.py`
+
+Current lines:
+
+- **L303‑405** – In `make_networkmodel_rhs` for combinatorial models, the JAX RHS preallocates arrays like `m = jnp.arange(max_states)` and loops over `max_states` and `max_trans` to compute phosphorylation and dephosphorylation fluxes[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/backend.py#:~:text=).
+
+Required change:
+
+- **Refactor** the JAX RHS to operate per protein rather than over a global `max_states`. For each protein, compute `jnp.arange(n_states[i])` locally and loop only over its own transitions (via the generator from `build_random_transitions`). Use `jax.lax.scan` or `while_loop` to iterate over states and transitions without materializing large arrays. Avoid storing `m` or `trans` arrays of size `max_states`. This reduces the memory footprint of compiled JAX functions while preserving identical dynamics. Because the per‑protein loops compute the same flux contributions, the model equations remain unchanged.
+
+- **Downcast** intermediate arrays to `float32` where safe, and ensure outputs are cast back to `float64` if required. Provide a configuration option to enable downcasting for memory‑constrained runs.
+
+### File: `networkmodel/sensitivity.py` (and similar modules storing trajectories)
+
+Current lines:
+
+- The `morris_analysis` function (approx lines 271‑400) stores every trajectory’s DataFrames (`prot_df`, `rna_df`, `phos_df`) in a list `trajectory_storage`, along with simulation metrics. Only the top‐ranked curves are used later, but all DataFrames remain in memory.
+
+Required change:
+
+- **Replace** the list of full DataFrames with an on‑disk store or stream the metrics. For example, write each simulation’s results to a temporary file (CSV/Parquet) and store only file paths and summary statistics in memory. After ranking, load only the necessary top trajectories. This ensures memory usage does not scale with the number of sensitivity samples. The numerical metrics remain identical.
+
+### File: `networkmodel/runner.py` (multi‑start logic)
+
+Current lines:
+
+- The `run_multistart` branch collects a pandas DataFrame with columns `X` and `F` for every start and appends them to a list. It may also keep entire simulation histories from each run.
+
+Required change:
+
+- **Modify** `run_multistart` so that after computing the objective value for a start, it retains only the parameter vector and objective scalar (or other minimal summary) in memory. Intermediate trajectories (`Y`, `states`, DataFrames) should be freed or written to disk immediately after evaluation. Optionally maintain an upper bound on the number of stored starts or use a streaming reducer to keep only the best `k` parameter sets. This change does not alter the optimization algorithm but prevents accumulating large data structures.
+
+## 6. Required Fixes
+
+### A. Fixes that only reduce memory materialization
+
+1. **Stream transitions** – Convert `build_random_transitions` to a generator and update ODE RHS and JAX backends to iterate over transitions lazily.
+
+2. Safe: ✅
+
+3. Risk: low
+
+4. Reason: the same transitions are computed; only the storage strategy changes.
+
+5. **Lazy** `bits` **computation** – Replace the dense `bits` matrix with per‑site weight vectors computed on demand in `simulate_and_measure`.
+
+6. Safe: ✅
+
+7. Risk: low
+
+8. Reason: computations use the same bitwise masks; results are identical.
+
+9. **Lazy** `S_cache` **and downcasting** – Allocate `S_cache` one column at a time or as a memory‑mapped array; downcast to `float32` where appropriate.
+
+10. Safe: ✅
+
+11. Risk: low
+
+12. Reason: `S_cache` is an intermediate used for event times; using a smaller dtype or on‑disk storage does not change results.
+
+13. **Per‑protein JAX loops** – Refactor JAX RHS to avoid global `max_states` arrays; use per‑protein loops and `jax.lax.scan`.
+
+14. Safe: ✅ (with careful testing)
+
+15. Risk: medium
+
+16. Reason: requires rewriting the JAX function; while math is the same, subtle errors could occur. Comprehensive tests must be run.
+
+17. **Memory guard on n_sites** – Add a guard that raises an informative error if `2^n_sites` exceeds a safe threshold (e.g., 256 states).
+
+18. Safe: ✅
+
+19. Risk: low
+
+20. Reason: prevents impossible runs; does not change correct behaviour for feasible models.
+
+### B. Fixes that prevent result/history retention
+
+1. **Streaming multi‑start results** – In `runner.py`, retain only essential summaries (parameter vector and objective) for each start; discard full trajectories and DataFrames after evaluation.
+
+2. Safe: ✅
+
+3. Risk: low
+
+4. Reason: the optimization algorithm uses only objective values; storing less history does not alter optimization logic.
+
+5. **Stream sensitivity trajectories** – In `sensitivity.py`, write each simulation’s results to disk and load only top trajectories when needed.
+
+6. Safe: ✅
+
+7. Risk: low
+
+8. Reason: analysis uses only selected trajectories; external storage preserves results without affecting ranking.
+
+### C. Fixes that add chunking or streaming
+
+1. **Chunked simulation extraction** – For very large `ns`, process subsets of states when computing per‑site signals. For example, divide `states` columns into manageable chunks and compute contributions sequentially.
+
+2. Safe: ✅ (provided chunk boundaries are handled correctly)
+
+3. Risk: medium
+
+4. Reason: chunking introduces additional loops; careful accumulation is required to avoid numerical drift.
+
+5. **Chunked hypercube enumeration** – When `build_random_transitions` is called for proteins with many sites, generate transitions in chunks (e.g., by enumerating states up to a maximum memory footprint).
+
+6. Safe: ✅
+
+7. Risk: medium
+
+8. Reason: enumeration order must remain consistent for reproducibility. Implementation should test chunk boundaries thoroughly.
+
+### D. Fixes that add safety guards
+
+1. **Early error on infeasible state sizes** – After calculating `n_states[i]`, check if `n_states[i] × time_points` would exceed a memory threshold (e.g., \>10^8 elements) and raise a `MemoryError` with guidance.
+
+2. Safe: ✅
+
+3. Risk: low
+
+4. Reason: prevents crashes by aborting early; does not alter valid runs.
+
+5. **Warn when JAX fallback is used** – If the JAX RHS cannot be compiled without full array materialization, print a warning and fall back to the numba implementation.
+
+6. Safe: ✅
+
+7. Risk: low
+
+8. Reason: does not change outputs; simply switches to a slower but safer backend.
+
+### E. Fixes that improve logging/profiling without changing computation
+
+1. **Diagnostic logging** – Add optional logging to report estimated memory for state vectors, transitions and bit masks before simulation. This allows the user to adjust parameters.
+
+2. Safe: ✅
+
+3. Risk: low
+
+4. Reason: adds diagnostics only.
+
+5. **Profile multi‑start memory usage** – Insert debug prints that show current memory usage after each start and automatically stop new starts if usage exceeds a threshold.
+
+6. Safe: ✅
+
+7. Risk: low
+
+8. Reason: does not alter optimization results; just aborts early to avoid crashes.
+
+## 7. What Must Not Change
+
+- **Model equations and mechanistic logic** – The differential equations for translation, dephosphorylation and phosphorylation in the combinatorial model must remain exactly as implemented. Only the representation of transitions and states may change.
+- **Optimization and ranking algorithms** – Multistart and sensitivity routines must still compute objective values and rank parameter sets identically. Only memory handling around these computations may change.
+- **Data formats and API** – Input file formats, command‑line interfaces and output data structures must be preserved. Any downcasting must be optional and off by default to maintain precision.
+- **Public semantics of combinatorial vs sequential/distributive models** – The conditions under which each model is selected must not change; the user must explicitly choose the combinatorial model.
+
+## 8. Validation Plan
+
+1. **Tiny combinatorial smoke test** – Create a minimal network with one protein and two phosphorylation sites (`n_sites=2`). Run the combinatorial model with and without the fixes and ensure that ODE trajectories, per‑site signals and objective values match to within numerical tolerance.
+2. **Medium‑size memory test** – Construct a network with three proteins, each with `n_sites=6`, and run a single simulation. Measure peak memory usage using `tracemalloc` or `psutil` before and after the fixes. The fixed version should consume significantly less memory (ideally \<1 GB for simulation extraction).
+3. **Comparison with distributive/sequential models** – Run the distributive and sequential models for the same network pre‑ and post‑fix. Confirm that their outputs and performance remain unchanged, demonstrating that the fixes are isolated to the combinatorial path.
+4. **Combinatorial numerical fidelity** – For a network with `n_sites=3`, compute the loss values and gradients using both the original and the fixed code paths. Assert that differences are below a small tolerance (e.g., `1e‑10` for loss and `1e‑8` for gradients).
+5. **Peak memory scaling** – Vary `n_sites` from 2 to 10 and record peak memory usage. Verify that memory scales roughly linearly with `2^n_sites` only while computing per‑site signals, not multiplied by the number of sites or time bins. Ensure there is no global `max_states` explosion.
+6. **Retention test** – Run a multistart optimization with 10 starts on the medium network and monitor memory usage across starts. Confirm that memory remains bounded and does not accumulate across starts.
+
+## 9. Expected Memory Impact
+
+Implementing the above fixes should dramatically reduce memory usage:
+
+- **Transition storage** – By streaming transitions instead of storing dense arrays, memory consumption drops from `O(2^n_sites × n_sites)` per protein to `O(1)` plus a small generator overhead.
+- **Bit‑mask computation** – Per‑site streaming removes the need for an `ns × n_sites` matrix; memory used becomes proportional to `ns` at most. For `n_sites=10`, this saves `2^10 × 10 × 8 bytes ≈ 80 KB` per protein per time point.
+- **Trajectory extraction** – Freeing state slices and storing only necessary signals reduces retained memory by orders of magnitude, especially in multistart and sensitivity loops.
+- **JAX backend** – Per‑protein loops avoid global arrays of size `max_states`; memory overhead becomes proportional to the largest protein rather than all proteins.
+- **Overall** – With these changes, running a medium combinatorial model (e.g., three proteins with `n_sites=6`) should require \<2 GB of RAM, enabling multi‑start optimization on typical servers.
+
+## 10. Implementation Checklist
+
+1. \[ \] Refactor `build_random_transitions` to yield transitions instead of returning dense arrays.
+2. \[ \] Update all RHS implementations (Numba and JAX) to iterate over transition generators.
+3. \[ \] Modify `System.__init__` to store transition iterators and allocate `S_cache` lazily or as a memory‑mapped array.
+4. \[ \] Add a memory guard in `Index.__init__` to warn or abort when `2^n_sites` exceeds a safe threshold.
+5. \[ \] Replace dense `bits` matrix construction with per‑site streaming in `simulate_and_measure` and free intermediate arrays promptly.
+6. \[ \] Refactor JAX combinatorial RHS to use per‑protein loops without global `max_states` arrays; test with `jax.lax.scan`.
+7. \[ \] Downcast intermediate combinatorial arrays to `float32` with a configuration flag and cast outputs back to `float64` when necessary.
+8. \[ \] Stream DataFrame and trajectory storage in sensitivity analysis; write temporary results to disk and keep only summaries in memory.
+9. \[ \] Stream multistart results; retain only top parameter sets and objective values in memory.
+10. \[ \] Add diagnostic logging and memory profiling utilities to inform users of estimated memory footprints and warn when thresholds are exceeded.
+11. \[ \] Write unit tests implementing the validation plan; ensure outputs match the original code and memory usage is reduced.
+
+[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,i) [\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,1%5D%29%2C%20dtype%3Dnp.float64) [\[6\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/network.py#:~:text=,1%5D%29%2C%20dtype%3Dnp.float64) raw.githubusercontent.com
+
+
+
+[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/simulate.py#:~:text=ns%20%3D%20int%28idx.n_states,p0%20%3D%20st%20%2B%201) raw.githubusercontent.com
+
+
+
+[\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/models.py#:~:text=def%20build_random_transitions%28idx%29%3A%20,for%20combinatorial%20topology) raw.githubusercontent.com
+
+
+
+[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/networkmodel/backend.py#:~:text=) raw.githubusercontent.com
+
+
+
+## 11. Implemented Memory-Safe Behavior
+
+The combinatorial implementation now keeps the same biological equations, objective values, ranking semantics, CLI options, input formats and output tables, while reducing unnecessary materialization:
+
+- `MODEL=2` still has an inherent `2^n_sites` state space. `Index` now validates each protein after computing `n_states = 1 << n_sites` and raises an informative `MemoryError` before simulation when the per-protein or total state dimension is unsafe. The error explains the exponential scaling and recommends reducing sites, choosing sequential/distributive models, or lowering workload.
+- Combinatorial transition order is unchanged: transitions are enumerated by state mask first and site index second, yielding only unset-bit phosphorylation edges. Large proteins no longer require dense `trans_from`, `trans_to` and `trans_site` arrays for RHS evaluation; dense arrays are retained only for small compatibility cases.
+- Combinatorial phosphosite signal extraction no longer builds the dense `ns x n_sites` bit matrix. Each site is aggregated with the equivalent bit-mask vector and temporaries are released immediately after use. This changes only memory materialization, not returned DataFrame columns or values.
+- `S_cache` for combinatorial mode is now a reusable current-site-rate buffer rather than an unconditional site-by-time dense matrix. The RHS receives the same current rates used by the previous time-bucketed calculation.
+- The JAX/Diffrax combinatorial RHS now generates phosphorylation edges per protein from local `n_states`/`n_sites` metadata instead of depending on global dense transition arrays. It still uses JAX-compatible loops and preserves differentiability through kinase scales and site rates.
+- Sensitivity trajectory retention is bounded to the configured top-curve count during simulation collection, so all trajectory DataFrames are not accumulated in memory. Output files and ranking/reporting semantics are preserved.
+- Multistart optimization already stores only bounded summaries and parameter vectors for starts (`summary`, `parameters`, `best` tables) rather than full trajectories or histories; no trajectory retention is introduced.
+
+### Interpreting guard errors
+
+A guard error means the requested combinatorial topology is too large to run safely in this process. Because every additional phosphosite doubles the number of protein state variables, safe settings depend primarily on the maximum number of sites on any single protein and the number of requested output time points. Recommended actions are:
+
+1. reduce the number of phosphosites included for high-degree proteins;
+2. use `MODEL=0` (distributive) or `MODEL=1` (sequential) when combinatorial state occupancy is not required;
+3. reduce workload such as the number of starts, sensitivity trajectories, or output times; and
+4. rerun after checking the combinatorial diagnostics in the log, which report proteins, `n_sites`, `n_states`, estimated total state dimension, estimated trajectory memory and the dense transition memory that is now avoided.
diff --git a/docs/Documentation/CHANGELOG.md b/docs/Documentation/CHANGELOG.md
deleted file mode 100644
index acea46a..0000000
--- a/docs/Documentation/CHANGELOG.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
-to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-> **Note for contributors:** The canonical changelog is `CHANGELOG.md` at the repository root.
-> This file (`docs/Documentation/CHANGELOG.md`) is a mirror kept in sync for the documentation site.
-> Always update the root `CHANGELOG.md` first, then propagate changes here.
-
-## [0.1.0] - 2025-04-19
-
-### Added
-
-- Initial release of **PhosKinTime**, a Python toolkit for ODE-based modeling of phosphorylation kinetics and
- transcriptional time-series.
-- Features include:
- - Parameter estimation.
- - Sensitivity analysis.
- - Steady-state computation.
- - Interactive visualization.
-- Support for Python 3.10, 3.11, and 3.12.
-- Dependencies include `numpy`, `pandas`, `seaborn`, `matplotlib`, `scipy`, and more.
-- CLI entry point `phoskintime` via `bin.main:main` in root.
-- Packaged directories: `bin`, `config`, `kinopt`, `models`, `paramest`, `plotting`, `sensitivity`, `steady`, `tfopt`,
- and `utils`.
-- Documentation and homepage available
- at [https://bibymaths.github.io/phoskintime/](https://bibymaths.github.io/phoskintime/).
-
-## [0.2.0] - 2025-04-24
-
-### Added
-
-- Added light grid in plotting of model.
-- Added `CHANGELOG.md`.
-- Added direct link to open file from CLI.
-- Added CLI wrappers for entry point.
-- Added deployment configuration file.
-- Added support for network via Cytoscape.
-- Added configuration file for PhosKinTime settings.
-- Enhanced analysis and plotting functions: added upper bound parameter, updated loss type defaults, and improved legend
- formatting.
-
-### Changed
-
-- Updated parameter bounds and model settings in configuration files.
-- Refactored logging statements and improved data filtering in main processing files.
-
-### Fixed
-
-- Fixed display of missing kinases in output before optimization in `kinopt`.
-
-### Removed
-
-- Removed clipping of predicted expression.
-- Deleted `abopt` directory.
-
-## [0.3.0] - 2025-04-24
-
-### Added
-
-- Support for non-psite time series in kinase data.
-- New results directory for structured output saving.
-- Detailed docstrings and inline documentation for key functions.
-
-### Changed
-
-- Refactored `powell.jl`: cleaner function names, improved parameter handling, and threading support.
-- Updated threading configuration and residuals calculation.
-- Replaced print statements with logger output in Python modules.
-- Adjusted beta bounds and default loss function settings.
-- Improved ODE system equations, plotting aesthetics, and documentation structure.
-
-### Fixed
-
-- LaTeX formatting in README.md.
-- Sheet name for estimated values in Excel export.
-
-### Removed
-
-- Obsolete `abopt` directory.
-- Outdated module references and unused code.
-- Removed julia implementation of kinopt
-- Removed Project.toml for Julia dependency management.
-
-## [0.4.0] - alpha
-
-Added
-- Phase space plots and strip plots for state distributions in sensitivity analysis.
-- Time-state grid visualization replacing old time-wise plots.
-- Enhanced logging format for parameter bounds and model configuration.
-- Increased number of trajectories to 10,000 for improved sensitivity resolution.
-- Support for parameter relationship plots and top parameter pair visualizations.
-
-Changed
-- Refactored sensitivity analysis functions and configuration parameters.
-- Updated site parameter labels and adjusted development mode flags.
-- Improved aesthetics of phase space and strip plots.
-- Adjusted ODE model references (`ODE_MODEL`) and refined output normalization logic.
-- Replaced hardcoded values with computed perturbations for sensitivity analysis.
-
-Fixed
-- Markdown formatting in README and PYPI_README.
-- Sheet name bug in Excel export for estimated values.
-
-Removed
-- Deprecated analysis modes and unused constants.
-- Combined time-weight calculation from data preprocessing.
-
-## [0.4.0] – 2025-06-05
-
-### Added
-
-* Model error plotting in the visualization pipeline.
-* Detailed descriptions for new `Y_METRIC` options in metric reports.
-
-### Changed
-
-* Streamlined argument parsing and enhanced logging details (now logs additional configuration parameters).
-* Updated default ODE model constant from `randmod` to `succmod` for improved simulation accuracy.
-* Disabled development mode by default; enhanced HTML‐report structure and log‐file handling.
-* Refactored output‐directory parameter to a list format and reorganized output files with improved naming conventions across display and plotting modules.
-* Updated upper bound ranges and default values for key parameters in config files.
-* Adjusted figure sizing and label management in plotting functions; updated regex for parameter‐label matching to support multi‐digit labels.
-* Improved plotting file naming and handling of perturbation values.
-* Enhanced Excel sheet processing for parameter imports and added regularization checks during data import.
-* Improved confidence‐interval logging format and enhanced regularization‐term computation in `normest.py`.
-* Updated lambda‐range handling in `normest.py`.
-
-### Fixed
-
-*None yet.*
-
-### Removed
-
-*None.*
-
diff --git a/docs/Documentation/dashboard.md b/docs/Documentation/dashboard.md
index 3defd4c..81c23dd 100644
--- a/docs/Documentation/dashboard.md
+++ b/docs/Documentation/dashboard.md
@@ -161,3 +161,9 @@ The dashboard includes an **Inference** tab when optional inference files are pr
- plots from `plots/multistart/`, `plots/profile_likelihood/`, and `plots/posterior/`
These are diagnostic scalar-objective summaries, not true multi-objective optimizer fronts.
+
+---
+
+## Unified no-code dashboard
+
+The newer unified dashboard lives in `dashboard/app.py` and can browse existing result directories, launch registered CLI/Pixi workflows, preview uploads, and show workflow-specific panels. See [No-code PhosKinTime dashboard](no_code_dashboard.md) for launch commands, supported workflows, result-directory contract details, upload behavior, examples, and troubleshooting.
diff --git a/docs/Documentation/no_code_dashboard.md b/docs/Documentation/no_code_dashboard.md
new file mode 100644
index 0000000..99a2272
--- /dev/null
+++ b/docs/Documentation/no_code_dashboard.md
@@ -0,0 +1,162 @@
+# No-code PhosKinTime dashboard
+
+The unified dashboard is a Streamlit application for browsing completed PhosKinTime runs and for launching existing CLI/Pixi workflows without editing Python code. It is intentionally a thin user interface: scientific fitting, ODE solving, optimization, and analysis remain in the existing backend modules and scripts.
+
+## Launching the dashboard
+
+From the repository root, start the dashboard with Pixi:
+
+```bash
+pixi run dashboard
+```
+
+For development with Streamlit reload-on-save enabled:
+
+```bash
+pixi run dashboard-dev
+```
+
+The tasks are defined in `pixi.toml` and run `streamlit run dashboard/app.py` with `PYTHONPATH=.` so the in-repository packages are importable.
+
+## Supported workflows
+
+The launcher registry exposes only structured, known commands. It builds argument lists, not shell strings, and runs subprocesses from the repository root.
+
+| Workflow | What it launches | Typical inputs |
+| --- | --- | --- |
+| `prep` | preprocessing cleanup task | files referenced by the current project config |
+| `kinopt-local` | `python -m kinopt.local` | phosphosite/protein tables, kinase network data, optional config |
+| `tfopt-local` | `python -m tfopt.local` | RNA/mRNA tables, TF network data, optional config |
+| `protwise-model` | `python -m protwise.runner.main` | protein/phosphosite data and optional config |
+| `networkmodel` | `python -m networkmodel.runner` | kinase network, TF network, omics tables, optional config |
+| `phoskintime-all` | `python -m config.cli all` | project-level configuration and data files |
+
+The dashboard does not invent new CLI flags. If an option is not listed in the workflow registry, it is not generated.
+
+## Result directory contract
+
+Each dashboard-launched run uses a predictable result directory under `results///` unless a workflow-specific backend chooses a compatible location. The browser expects this contract:
+
+```text
+results//
+├── metadata.json
+├── command.txt
+├── console.log
+├── config_resolved.yaml
+├── tables/
+├── plots/
+├── logs/
+├── reports/
+└── artifacts/
+```
+
+The result browser also recognizes practical legacy layouts, including Networkmodel top-level CSVs such as `scalar_objective.csv`, `convergence_history.csv`, prediction CSVs, `optimization/`, `profiles/`, `posterior/`, and legacy `kinopt_results.xlsx` or `tfopt_results.xlsx` files.
+
+## Upload behavior
+
+The upload panel accepts `.csv`, `.tsv`, `.xlsx`, `.yaml`, `.yml`, `.json`, and `.txt` files for upload/preview or preset workflows. Files assigned to workflow `--conf` fields currently must be `.toml` because the target runners parse TOML. Uploaded files are copied to a per-run folder:
+
+```text
+dashboard_uploads//
+```
+
+Uploads are not written into `data/` and are not modified silently. The dashboard checks for empty files, unsupported extensions, unreadable files, and duplicate filenames after sanitization. CSV, TSV, and Excel inputs can be previewed with pandas before they are assigned to a workflow.
+
+Workflow-specific validation follows the backend readers. ProtWise protein input and Networkmodel kinase network, TF network, MS/protein, RNA, and phosphoproteomics inputs must be CSV files because those runners read them with `pandas.read_csv`. Excel remains valid only for workflow inputs that are actually read as Excel, such as ProtWise phosphosite/RNA files or previous KinOpt/TFOpt result workbooks.
+
+## Command preview and execution
+
+Before a run starts, the dashboard shows the exact command preview and the underlying argument list. This keeps runs reproducible and helps users copy the command into a terminal if preferred.
+
+During execution, stdout and stderr are streamed into the page and written to `console.log` in the run directory. The runner records `command.txt` and launcher metadata in `metadata.json`. Failures are displayed with the return code and a log tail rather than crashing the Streamlit session.
+
+## Result browser usage
+
+Use the **Browse results** tab to select an existing result directory. The browser displays:
+
+- `metadata.json` with `st.json` when valid;
+- `command.txt`, `console.log`, and other logs as text/code;
+- CSV, TSV, and Excel tables with `st.dataframe`;
+- PNG, JPG, JPEG, SVG, and HTML plots;
+- Markdown, HTML, and PDF reports where Streamlit can render them;
+- artifacts as downloadable files;
+- a ZIP download button for the selected result directory.
+
+If standard contract files are missing, the browser reports them clearly and still attempts to show recognized legacy outputs.
+
+## Workflow-specific panels
+
+The result browser includes workflow tabs for KinOpt, TFOpt, ProtWise, Networkmodel, and advanced analyses.
+
+- **KinOpt** displays discovered KinOpt result workbooks, alpha/beta-related tables where available, observed-vs-estimated output files, and existing plots.
+- **TFOpt** displays TFOpt result workbooks, TF alpha/beta or equivalent tables, latent activity/dominance/knockout outputs when present, and existing plots.
+- **ProtWise** displays ODE prediction tables, residual/fits files, sensitivity outputs, and generated plots or reports.
+- **Networkmodel** supports both the standardized result layout and legacy Networkmodel outputs such as `dashboard_bundle.pkl`, scalar objective CSVs, convergence history, predictions, optimization summaries, profiles, and posterior outputs.
+- **Advanced analysis** exposes parameterized command wrappers for existing scripts such as curve similarity, protein accumulator detection, subnetwork export, mechanistic insights, temporal sensitivity, and mechanism comparison. These analyses are not run automatically on page load.
+
+Optional visualization packages are imported only when the affected panel is used. If a dependency is unavailable, the dashboard should show a clear message instead of failing during import.
+
+## Examples
+
+### Browse an existing result directory
+
+1. Run `pixi run dashboard`.
+2. Open **Browse results**.
+3. Enter the base results folder, for example `results`.
+4. Select a run folder.
+5. Review metadata, tables, plots, logs, reports, artifacts, and workflow-specific tabs.
+
+### Run KinOpt
+
+1. Open **Run workflow**.
+2. Select `KinOpt local (kinopt-local)`.
+3. Upload or select phosphosite/protein and kinase-network inputs as required by the current CLI/config.
+4. Preview uploaded tables.
+5. Confirm validation passes.
+6. Review the command preview.
+7. Click **Run workflow**.
+8. When the process completes, the result browser opens the run directory.
+
+### Run TFOpt
+
+1. Select `TFOpt local (tfopt-local)`.
+2. Upload or select RNA/mRNA and TF-network inputs.
+3. Pass a config file through the existing config argument when needed.
+4. Review and run the generated command.
+5. Inspect TFOpt tables, plots, logs, and workflow tabs after completion.
+
+### Run ProtWise/model
+
+1. Select `ProtWise model (protwise-model)`.
+2. Assign the required protein/phosphosite inputs and optional config file.
+3. Preview the command and run it.
+4. Use the ProtWise panel to inspect predictions, residuals, fits, sensitivity files, plots, and reports.
+
+### Run Networkmodel
+
+1. Select `Networkmodel (networkmodel)`.
+2. Assign kinase-network, TF-network, omics, and config inputs supported by the CLI.
+3. Review the structured command preview.
+4. Run the workflow.
+5. Inspect scalar objective, convergence, predictions, optimization/profile/posterior outputs, plots, and the dashboard bundle when available.
+
+### Download a result ZIP
+
+1. Open a result directory in **Browse results**.
+2. Open the **Download** tab.
+3. Click **Prepare ZIP archive**.
+4. Click **Download result ZIP**.
+
+The ZIP is generated in memory and is not committed to the repository.
+
+## Troubleshooting
+
+| Symptom | Suggested action |
+| --- | --- |
+| Dashboard command is unavailable | Confirm Pixi is installed and run commands from the repository root. |
+| Result directory is missing | Check the selected folder path and whether the workflow wrote to the expected `results///` directory. |
+| `metadata.json` is missing or malformed | The browser can still show files, but provenance is incomplete. Re-run through an updated CLI wrapper when reproducibility is required. |
+| CSV/TSV/XLSX preview fails | Verify the file is not empty, is readable, and matches its extension. |
+| Workflow fails | Review the return code and `console.log` tail shown by the launcher. Copy the command preview into a terminal for deeper debugging if needed. |
+| Optional visualization is unavailable | Install the optional Pixi environment or dependency for that visualization; core browsing remains available. |
+| Large HTML/report previews are truncated | Download the original file from the browser or result ZIP. |
diff --git a/docs/Documentation/scripts.md b/docs/Documentation/scripts.md
index b26d263..1d0c974 100644
--- a/docs/Documentation/scripts.md
+++ b/docs/Documentation/scripts.md
@@ -220,3 +220,11 @@ High sample counts significantly increase computation time.
`networkmodel/config.py`.
- The `compare_mechanisms.py` script requires additional dependencies (`gravis`, `networkx`,
`imageio`) not included in the base `requirements.txt`.
+
+## Dashboard-ready output directories
+
+Major backend workflows accept an explicit `--outdir`/`--output-dir` option so automated tools and the planned no-code dashboard can discover outputs consistently. A run directory contains `metadata.json`, `command.txt`, `console.log`, `config_resolved.yaml` when applicable, and the standard `tables/`, `plots/`, `logs/`, `reports/`, and `artifacts/` subdirectories. Legacy filenames are retained at the run-directory root where existing scripts or documentation rely on them, with dashboard-facing copies mirrored into the standard subfolders.
+
+## Unified dashboard integration and legacy script status
+
+The unified dashboard now provides workflow-specific result panels for KinOpt, TFOpt, ProtWise, Networkmodel, and advanced analyses. Legacy visualization/readout scripts such as `kinopt_network_viz.py`, `tfopt_network_viz.py`, `kinopt_network_readout.py`, and `tfopt_network_readout.py` are retained for batch reproducibility, but interactive exploration should prefer the unified dashboard panels and the existing `app/kinopt.py` / `app/tfopt.py` logic where available. Analysis scripts remain callable backends; the dashboard builds parameterized commands and writes outputs under the selected run directory instead of duplicating scientific computations in Streamlit.
diff --git a/docs/PhosKinTime_Extension_Planning.md b/docs/PhosKinTime_Extension_Planning.md
new file mode 100644
index 0000000..f7c022b
--- /dev/null
+++ b/docs/PhosKinTime_Extension_Planning.md
@@ -0,0 +1,839 @@
+# 1. Drug Dose‑Response / PK‑PD / QSP Extension
+
+## Executive summary
+
+This extension adds a pharmacokinetics–pharmacodynamics (PK‑PD) layer to PhosKinTime so that perturbations by small‑molecule drugs can be modelled in a dose‑ and time‑dependent manner. The existing `networkmodel` integrates mRNA, total protein and phosphorylation dynamics and estimates parameters such as kinase activity multipliers and production/degradation constants[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=,factor%20scaling%20parameter). A PK‑PD extension would incorporate an exogenous drug concentration profile and link it to specific kinetic parameters or activities in the ODE system. For example, drug concentrations could modulate kinase activity multipliers (`c_k`) or degradation rates (`D_i`) through Hill/Emax functions. The extension does **not** claim to derive drug binding affinities from phosphorylation data nor replace dedicated PK modelling; it treats drug concentration as an external input that alters existing ODE terms.
+
+## Why this is or is not close to the current framework
+
+- **Classification:** medium‑term extension.
+- **Justification:** The current framework already solves ODEs for protein/mRNA/phospho trajectories and fits parameters using JAX-based optimization[\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=predicted%20change%20should%20be%20interpreted,not%20as%20a%20standalone%20correlation). It outputs kinase activity multipliers and turnover parameters[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=,factor%20scaling%20parameter) that could be modulated by drugs. However, no PK modelling exists, and drug effects require adding new state variables (e.g., drug compartments) and dose schedules. Therefore this extension extends the core ODE system with additional compartments but remains conceptually compatible with the existing solver architecture.
+
+## Current PhosKinTime assets this can reuse
+
+- **Fitted kinetic parameters:** `c_k`, `A_i`, `B_i`, `C_i`, `D_i`, `Dp_i`, `E_i` and `tf_scale` from `networkmodel` outputs[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=,factor%20scaling%20parameter).
+- **Predicted trajectories:** time-indexed tables for protein, mRNA and phospho states[\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%60%60%60text%20TIME_POINTS_PROTEIN%20,phospho%20fitted%20trajectories).
+- **Residuals and sensitivity analysis utilities:** residual outputs compare observed vs. predicted values[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=), and sensitivity analysis perturbs fitted parameters to assess influence[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=). Drug effects could reuse these analyses to quantify impact.
+- **Optimization infrastructure:** the `kinopt` and `tfopt` modules provide global and local optimization frameworks with constraint handling[\[6\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/kinopt/README.md#:~:text=%2A%2Akinopt%2A%2A%20provides%20an%20end,for) and can be adapted to fit PK‑PD parameters.
+- **Configuration system:** `config.toml` defines time grids and parameter bounds[\[7\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,120%2C%20240%2C%20480%2C%20960)[\[8\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,0); these can be extended to include dose schedules and PK parameters.
+- **Export utilities:** existing export functions for kinase activity and phosphorylation rates[\[9\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) can be extended to include drug concentration and effect summaries.
+
+## Required new scientific layer
+
+A PK‑PD module must model drug absorption, distribution, metabolism and elimination as additional ODEs. At minimum, a one‑compartment model with first‑order elimination can be used:
+
+- **PK model:**
+
+
+- $$\frac{dC_{drug}}{dt} = \frac{D(t)}{V} - k_{el}C_{drug}$$
+ where `D(t)` is the dosing rate, `V` is the apparent volume of distribution and `k_{el}` is the elimination rate.
+
+
+- **PD link:** drug concentration modifies specific kinetic parameters through a Hill or Emax function, e.g.,
+
+
+- $$c_{k}^{\text{eff}} = c_{k} \times \frac{1}{1 + \left( C_{drug}/IC_{50} \right)^{n}}$$
+ for inhibition of kinase `k`, or
+
+ $$D_{i}^{\text{eff}} = D_{i} + E_{max} \times \frac{C_{drug}}{EC_{50} + C_{drug}}$$
+ for enhanced degradation.
+
+
+- **Additional states:** multi‑compartment PK models (central/peripheral) or metabolite compartments could be added later.
+
+- **Model integration:** the extended ODE system will include the drug compartment and modify the right-hand sides of existing equations via the PD link. The model remains deterministic and uses the same solver (Diffrax adaptive RK45). Parameter estimation may involve fitting `k_el`, `IC_50`, `n`, `EC_50` and `E_max` given experimental dose‑response data.
+
+## Realistic inputs
+
+| Filename | Required columns | Optional columns | Units | Source | Notes |
+|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------|--------------------|-----------------------------------------------|-------------------------------------------------------------------------------------|
+| `dose_schedule.csv` | `time`, `dose_amount` | `route`, `duration` | hours, mg | User-provided experiment or clinical protocol | Defines when and how much drug is administered. `route` may adjust bioavailability. |
+| `pk_params.csv` | `drug`, `V` (L), `k_el` (1/hr) | `k_a` (absorption rate), `F` (bioavailability) | as indicated | Literature or prior PK data | Baseline PK parameters; can be overridden by fitting. |
+| `pd_targets.csv` | `target_type` (`kinase`, `protein`, `tf`), `target_id`, `effect_type` (`inhibition`, `activation`), `IC50` or `EC50`, `n` (Hill coefficient) | `E_max` | concentration (µM) | DrugBank, ChEMBL for IC50/EC50 values | Maps the drug to specific parameters in PhosKinTime outputs. |
+| `pkpd_measured.csv` | `time`, `concentration` | `effect_measure` | hours, µM | Experimental dose‑response studies | Used to fit PK and PD parameters. |
+
+The extension also uses existing outputs (`kinase_activities.csv`, `predicted_protein.csv`, etc.) to map PD effects onto dynamic variables.
+
+## Realistic input sources
+
+- **DrugBank, ChEMBL, BindingDB:** provide IC50/EC50 and Hill coefficients for drug‑target interactions.
+- **PK data repositories:** e.g., PharmGKB or published PK studies for elimination and distribution parameters.
+- **LINCS/L1000 and CPTAC:** for dose‑response transcriptome and phospho‑proteome data to fit PD effects.
+- **Existing PhosKinTime outputs:** predicted kinase activities and protein/phospho trajectories serve as baseline without drug intervention.
+
+## Outputs
+
+| Output file | Description |
+|--------------------------|---------------------------------------------------------------------------------------------------------------|
+| `pkpd_concentration.csv` | Simulated drug concentration versus time for each compartment. |
+| `pkpd_effects.csv` | Effective parameter values (e.g., inhibited `c_k`, modified `D_i`) over time. |
+| `pkpd_trajectories.csv` | Predicted protein, mRNA and phospho trajectories under drug perturbation. |
+| `pkpd_residuals.csv` | Differences between measured and predicted responses in dose‑response experiments. |
+| `pkpd_sensitivity.csv` | Sensitivity of each PD parameter on outcome metrics (e.g., area under curve). |
+| `pkpd_report.json` | Metadata: model version, input sources, PK/PD parameter estimates and uncertainty. |
+| `pkpd_plots/` | Visualizations of dose schedules, concentration‑time curves, dose‑response curves and overlayed trajectories. |
+
+All tables should follow the same tidy format as existing residual and trajectory tables[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=).
+
+## Proposed repository structure
+
+ extensions/
+ dose_response/
+ __init__.py
+ pk_models.py # defines one‑ and two‑compartment PK models
+ pd_links.py # functions linking drug concentration to kinetic parameters
+ simulate.py # integrates PK and original ODE system
+ fit.py # fits PK/PD parameters to dose‑response data
+ export.py # writes outputs and plots using existing export utilities
+ inputs.py # loaders/validators for PK/PD input tables
+ config.py # default configuration schema
+ cli.py # entry point for command line execution
+ plots.py # dose‑response and PK/PD specific plots
+ tests/
+ test_pkpd_models.py
+ test_integration.py
+
+This `extensions` top‑level directory keeps domain‑specific code separate from the core. Shared utilities (e.g., plotting) can import from `networkmodel.export` and `protwise.plotting`.
+
+## Configuration design
+
+Add a `[dose_response]` block to `config.toml`:
+
+ [dose_response]
+ # List of drug names; order corresponds to pk_params and pd_targets
+ name = ["drugA"]
+ # Dose schedule file path (required)
+ dose_schedule = "data/dose_schedule.csv"
+ # PK parameter file (can be estimated)
+ pk_params = "data/pk_params.csv"
+ # PD target mapping file
+ d_targets = "data/pd_targets.csv"
+ # Measured concentration and response data for fitting (optional)
+ data = "data/pkpd_measured.csv"
+ # Default PK model: one_compartment, two_compartment
+ model = "one_compartment"
+ # Solver options for PK integration
+ solver = {atol = 1e-6, rtol = 1e-5}
+ # Output directory relative to run directory
+ outdir = "results/pkpd/"
+
+Comments follow the style of existing config sections[\[7\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,120%2C%20240%2C%20480%2C%20960). Additional keys (e.g., `bounds` for `IC50`) can be added later.
+
+## CLI design
+
+- **Python module:** `phoskintime.extensions.dose_response.cli` with click/argparse interface.
+- **Command:**
+
+
+- python -m phoskintime dose-response --conf config.toml
+
+
+- **Pixi task:** Add to `pixi.toml`:
+
+
+- [[task]]
+ name = "dose-response"
+ cmd = "python -m phoskintime dose-response --conf config.toml"
+ category = "analysis"
+
+
+- Arguments: `--fit` to estimate PK/PD parameters; `--simulate` to run simulation with fixed parameters; `--plot` to generate plots.
+- Output directory: created as `//` with subfolders `trajectories`, `plots`, `metrics`, following existing result conventions.
+
+## Minimal viable implementation
+
+- **Inputs:** `dose_schedule.csv` with a single bolus dose; `pd_targets.csv` with one kinase target and an `IC50` value; baseline model outputs.
+- **Algorithm:** One‑compartment PK with fixed `V` and `k_el`; compute drug concentration; apply a simple Emax inhibition to `c_k`; re-simulate networkmodel ODE to obtain trajectories; compute residuals vs. baseline.
+- **Outputs:** Concentration curve; inhibited `c_k` values over time; perturbed trajectories; simple plots.
+- **Tests:** unit tests for PK differential equation; integration test verifying that increasing dose decreases phosphorylation output; tests verifying correct loading of input tables.
+
+## Full implementation roadmap
+
+- **v0.1:** Add file loaders, one‑compartment PK model, Emax PD link and CLI wrapper; produce concentration and effect outputs; include minimal tests.
+- **v0.2:** Support multi‑compartment PK, Hill functions and multiple targets; allow fitting `k_el`, `IC50` using measured data; integrate with `kinopt`/`tfopt` optimization to co‑fit PD parameters.
+- **v0.3:** Add plotting utilities (dose‑response curves, sensitivity analysis) and export to dashboard; implement configuration validation.
+- **v0.4:** Include advanced QSP features such as metabolite compartments or saturable elimination; integrate with `BayesianInference` to estimate posterior distributions; provide example notebooks.
+- **v1.0:** Stable release with documentation, reproducible examples and cross‑validation on published drug perturbation datasets.
+
+## Validation strategy
+
+- **Unit tests:** verify PK ODE solutions against analytical solutions for bolus dosing; check PD scaling functions produce expected limiting values.
+- **Integration tests:** run simulation with and without drug and ensure the model output changes appropriately (e.g., decreased phosphorylation when inhibiting kinases).
+- **Synthetic data tests:** generate synthetic dose‑response curves using known PK/PD parameters and ensure the fitter recovers them within tolerance.
+- **Regression tests:** fix a seed and verify that changes in implementation do not alter existing results.
+- **Biological sanity checks:** compare predicted IC50 and elimination half‑lives with literature values; check that predicted changes in kinase activity multipliers are plausible relative to measured potency.
+- **External benchmarks:** where possible, compare simulated responses to LINCS/CPTAC dose‑response experiments for the same drugs.
+
+## Risks and failure modes
+
+- **Parameter non‑identifiability:** PK and PD parameters may be poorly identifiable from limited dose‑response data, leading to wide uncertainty.
+- **Scale mismatch:** integrating PK models can introduce timescales (minutes/hours) different from cellular phosphorylation timescales (minutes); solver stiffness may increase.
+- **Insufficient data:** high‑quality dose‑response phosphoproteomics is scarce; the extension may rely on assumptions about potency.
+- **Overfitting:** fitting PK/PD parameters concurrently with network parameters may overfit small datasets.
+- **Improper mapping:** incorrect mapping of drug targets to model parameters could misrepresent mechanism of action.
+
+## What not to do
+
+- Do **not** infer drug binding affinities from phosphorylation data alone.
+- Do **not** claim that the PK model is physiologically accurate beyond the simple compartment(s) defined.
+- Do **not** hard‑code drug‑specific values; keep parameters configurable.
+- Do **not** alter the core `networkmodel` equations; extend them through a wrapper.
+- Do **not** ignore parameter bounds defined in existing configuration files[\[8\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,0).
+
+## Recommended priority
+
+**Second**. Extending PhosKinTime with a PK‑PD layer is technically feasible and directly relevant for pharmacological studies. The required ODE and optimization infrastructure aligns with the existing framework, but the development effort is moderate due to new compartments and external data dependencies.
+
+# 2. mRNA Vaccine / Antigen‑Expression / Immune‑Response Extension
+
+## Executive summary
+
+This extension models how synthetic mRNA vaccines or gene therapy constructs drive antigen expression and trigger innate immune responses. PhosKinTime currently simulates endogenous mRNA, protein and phosphorylation dynamics[\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%60%60%60text%20TIME_POINTS_PROTEIN%20,phospho%20fitted%20trajectories). The vaccine extension would add an exogenous mRNA species encoding an antigen; its translation would produce antigenic protein, which could be processed and presented to immune effectors. The framework would then simulate downstream signalling pathways (e.g., interferon responses) by introducing additional state variables for cytokines and immune modulators. The extension does **not** claim to model adaptive immunity (e.g., T cell clonal expansion) or to predict vaccine efficacy; it focuses on early antigen expression and innate signalling in the same timescale as phospho‑proteomics experiments.
+
+## Why this is or is not close to the current framework
+
+- **Classification:** downstream annotation layer / medium-term extension.
+- **Justification:** The current ODE system describes gene expression and post‑translational modifications within cells. Adding an exogenous mRNA vaccine is conceptually similar to adding a new mRNA/protein species but introduces novel processes: exogenous mRNA uptake, translation, antigen processing and innate immune sensing. A minimal model could reuse existing translation and degradation parameters but needs new states for antigen and cytokine species and may require coupling to pattern‑recognition receptor pathways. Therefore it sits at a medium distance from the core but remains within dynamic modelling.
+
+## Current PhosKinTime assets this can reuse
+
+- **Translation kinetics:** existing parameters for mRNA degradation (`B_i`) and protein production (`C_i`) and turnover (`D_i`)[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=,factor%20scaling%20parameter) can be reused to simulate antigen translation and degradation.
+- **Predicted trajectories and residual analyses:** existing pipeline writes mRNA and protein fitted trajectories[\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%60%60%60text%20TIME_POINTS_PROTEIN%20,phospho%20fitted%20trajectories) and residuals[\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=). These utilities can be reused to visualise antigen expression and immune response curves.
+- **Sensitivity analysis infrastructure:** to quantify which kinetic parameters (e.g., vaccine dose, translation rate) most influence antigen peak concentration[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=).
+- **Dashboard and export functions:** can be extended to include antigen and cytokine trajectories in interactive dashboards.
+- **Configuration and CLI patterns:** reuse the `config.toml` schema and CLI skeleton for new tasks[\[7\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,120%2C%20240%2C%20480%2C%20960).
+
+## Required new scientific layer
+
+- **Exogenous mRNA uptake and decay:** a new state variable `M_v(t)` representing vaccine mRNA copies with uptake kinetics and degradation rate `k_dec_v`.
+
+$$\frac{dM_{v}}{dt} = u_{v}(t) - k_{dec\_ v}M_{v}$$
+
+where `u_v(t)` is the dosing schedule.
+
+- **Antigen translation and degradation:** a new protein state `P_v(t)` produced from `M_v` with translation rate `k_trans_v` and degraded at `k_deg_v`.
+
+- **Innate immune sensing:** simplified pattern‑recognition receptor activation triggered by `M_v` or `P_v` could be modelled as a sigmoidal function producing an interferon or cytokine state `I(t)`.
+
+$$\frac{dI}{dt} = k_{I}\frac{M_{v}^{h}}{K_{I}^{h} + M_{v}^{h}} - k_{clear}I$$
+
+- **Impact on network:** cytokine signalling could modulate transcription factors (`tf_scale`), basal production (`A_i`) or degradation parameters (`B_i`,`C_i`) for select genes. A mapping file would specify which PhosKinTime parameters are impacted.
+
+- **Optional modules:** translation efficiency modifiers for codon optimisation, mRNA secondary structure stability, or lipid nanoparticle delivery could be incorporated in later versions.
+
+## Realistic inputs
+
+| Filename | Required columns | Optional columns | Units | Source | Notes |
+|--------------------------|-----------------------------------------------------------------|------------------------|--------------------|--------------------------------------------------------------|-------------------------------------------------------------------------------|
+| `vaccine_schedule.csv` | `time`, `dose_mRNA` | `formulation`, `route` | hours, µg | User‑provided | Specifies injection time and dose; `formulation` may affect uptake rate. |
+| `vaccine_params.csv` | `k_uptake`, `k_dec_v`, `k_trans_v`, `k_deg_v` | `codon_opt_score` | rate constants | Literature on mRNA vaccine kinetics; pre‑clinical PK/PD data | Parameter guesses or priors for vaccine mRNA translation and degradation. |
+| `immune_params.csv` | `k_I`, `K_I`, `h`, `k_clear` | `target_mapping` | as indicated | Immunology literature | Defines cytokine dynamics. |
+| `antigen_annotation.csv` | `gene`, `antigen_peptide`, `MHC_allele`, `immunogenicity_score` | | sequences/affinity | IEDB, ImmPort | Connects antigen translation products to known epitopes. |
+| `immune_targets.csv` | `target_type`, `target_id`, `effect`, `magnitude` | | dimensionless | Expert‑curated | Maps cytokine levels to modifications of PhosKinTime parameters. |
+| `vaccination_data.csv` | `time`, `measured_antigen`, `measured_cytokine` | | hours | Experimental data | For fitting vaccine kinetic parameters and validating immune response curves. |
+
+## Realistic input sources
+
+- **Immune Epitope Database (IEDB):** epitope sequences, MHC binding affinities and immunogenicity scores.
+- **ImmPort and Expression Atlas:** innate immune response time courses after mRNA vaccination.
+- **GEO / ArrayExpress:** transcriptomic and proteomic datasets post‑vaccination for parameter fitting.
+- **Vaccine PK studies:** published data on mRNA stability and translation kinetics.
+
+## Outputs
+
+| Output file | Description |
+|--------------------------------|---------------------------------------------------------------------------------------------|
+| `vaccine_antigen.csv` | Simulated antigen mRNA and protein levels vs. time. |
+| `vaccine_immune.csv` | Innate immune mediator (e.g., interferon) levels vs. time. |
+| `vaccine_effects.csv` | Effective modifications to PhosKinTime parameters (e.g., `tf_scale` modulation). |
+| `vaccination_trajectories.csv` | Predicted trajectories of endogenous mRNA, protein and phospho species under vaccination. |
+| `vaccine_residuals.csv` | Differences between measured and predicted antigen and cytokine levels. |
+| `vaccine_plots/` | Plots of antigen expression and immune response; overlay with experimental data. |
+| `vaccine_report.json` | Metadata summarizing vaccine kinetics parameters, immunogenicity annotation and provenance. |
+
+## Proposed repository structure
+
+ extensions/
+ vaccine/
+ __init__.py
+ mRNA.py # vaccine mRNA uptake and decay models
+ antigen.py # translation and degradation of antigen protein
+ immune.py # innate immune sensing and cytokine dynamics
+ coupling.py # maps immune mediators to PhosKinTime parameters
+ simulate.py # integrates vaccine and network ODEs
+ fit.py # fits kinetic parameters using experimental data
+ export.py # writes outputs and generates plots
+ inputs.py # validators for vaccine-related tables
+ config.py # configuration schema
+ cli.py # command-line interface
+ tests/
+ test_vaccine_models.py
+ test_coupling.py
+
+The folder lives under `extensions` to avoid polluting core modules. `simulate.py` will import the existing `networkmodel` solver and apply parameter modifications.
+
+## Configuration design
+
+Add a `[vaccine]` section to `config.toml`:
+
+ [vaccine]
+ # Path to dosing schedule
+ dose_schedule = "data/vaccine_schedule.csv"
+ # Parameter files
+ vaccine_params = "data/vaccine_params.csv"
+ immune_params = "data/immune_params.csv"
+ antigen_annotation = "data/antigen_annotation.csv"
+ immune_targets = "data/immune_targets.csv"
+ # Output directory
+ outdir = "results/vaccine/"
+ # Flags
+ fit = true # fit kinetic parameters if data present
+ use_codons = false # apply codon optimisation modifiers
+
+Each parameter file contains columns described above. Comments and keys follow existing style[\[7\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,120%2C%20240%2C%20480%2C%20960).
+
+## CLI design
+
+- **Python module:** `phoskintime.extensions.vaccine.cli`.
+- **Command:**
+
+
+- python -m phoskintime vaccine --conf config.toml [--fit] [--simulate]
+
+
+- **Pixi task:** add `vaccine` under `[[task]]` with the same command.
+- Optional flags: `--fit` fits kinetic parameters, `--simulate` runs simulation with given parameters, `--plot` draws antigen and immune curves.
+- Output directories follow the pattern `//` with subdirectories for trajectories, plots and metrics.
+
+## Minimal viable implementation
+
+- **Inputs:** A single vaccine dose schedule; default parameter values for uptake, translation and immune sensing; mapping file linking interferon to a global transcription factor scaling parameter.
+- **Algorithm:** Solve additional ODEs for `M_v`, `P_v` and `I` using explicit Euler or Diffrax integrator; update `tf_scale` or `A_i` in `networkmodel` with a linear function of `I`; run simulation; produce antigen and immune curves and overlay with any measured data.
+- **Outputs:** Antigen and immune trajectories; predicted modifications to gene expression; simple plots.
+- **Tests:** unit tests ensuring the antigen ODE yields expected exponential decay; integration test verifying that increasing vaccine dose increases antigen levels; test that immune coupling modifies network parameters.
+
+## Full implementation roadmap
+
+- **v0.1:** Implement basic mRNA and antigen ODEs and coupling to `tf_scale`; produce antigen and immune plots; write CLI and configuration; include tests.
+- **v0.2:** Include innate immune sensing via Hill kinetics; allow parameter fitting to measured cytokine data; support multiple antigens; integrate codon optimisation modifiers.
+- **v0.3:** Add mapping between antigen sequences and immunogenicity scores using IEDB; export epitope annotation; include interactive dashboard elements.
+- **v0.4:** Extend to poly‑epitope constructs; include simulation of memory cytokines; provide example notebooks with vaccination time courses.
+- **v1.0:** Publish validated extension with real vaccine datasets and cross‑validated predictions.
+
+## Validation strategy
+
+- **Unit tests:** check ODE solutions and coupling functions; ensure codon optimisation multiplier behaves correctly.
+- **Integration tests:** simulate a simple vaccination scenario and verify that antigen and immune curves follow expected kinetics; ensure coupling modifies gene expression in network outputs.
+- **Synthetic data tests:** create synthetic vaccine and immune time courses; test that fitting recovers the known parameters.
+- **Biological sanity checks:** compare simulated antigen half‑life and interferon dynamics with published data; ensure predicted modulation of transcription factor scaling falls within realistic bounds.
+- **External benchmarks:** if datasets from vaccine studies are available, fit the model and compare predicted cytokine profiles and gene expression changes.
+
+## Risks and failure modes
+
+- **Complexity of immune responses:** innate immune signalling involves many pathways; the simplified model may capture only a subset of dynamics.
+- **Data scarcity:** high‑quality time‑series data for antigen translation and cytokine levels are limited; parameter estimates may be uncertain.
+- **Coupling assumptions:** mapping interferon to transcription factor scaling may be simplistic; real regulatory effects are gene‑specific.
+- **Parameter identifiability:** multiple parameters (uptake, degradation, translation) might yield similar antigen trajectories.
+- **Numerical stiffness:** addition of rapid immune reactions could stiffen the ODE system and require careful solver settings.
+
+## What not to do
+
+- Do **not** attempt to model adaptive immunity or antibody affinity maturation; this extension focuses on early cellular responses.
+- Do **not** claim to predict vaccine efficacy or protective immunity from phosphorylation data.
+- Do **not** incorporate proprietary vaccine formulation details; only publicly available parameters should be included.
+- Do **not** modify core `networkmodel` files; implement coupling through wrappers.
+- Do **not** ignore mRNA translation and degradation parameters when simulating antigen production.
+
+## Recommended priority
+
+**Later.** While scientifically interesting, this extension introduces new biological domains (vaccine delivery and innate immunity) and requires external data and assumptions. It should be implemented after a PK‑PD extension and structural annotations are in place.
+
+# 3. Antibody Perturbation / Antibody‑Response Extension
+
+## Executive summary
+
+This extension models how exogenous antibodies bind to cellular proteins or phospho‑epitopes and perturb signalling dynamics. PhosKinTime currently predicts phosphorylation rates and kinase activities[\[9\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=), but does not represent antibody binding or immune responses. The antibody extension would introduce binding kinetics (association and dissociation) between antibodies and their targets, reducing the availability or activity of the bound proteins. It can simulate neutralisation (removal of active protein) or crosslinking (aggregation). The extension does **not** perform antibody design; it uses user‑supplied binding parameters and epitope mapping.
+
+## Why this is or is not close to the current framework
+
+- **Classification:** downstream annotation layer / external pipeline integration.
+- **Justification:** PhosKinTime focuses on intracellular kinetics; antibody perturbation acts at the extracellular or cell‑surface level and may not be captured by existing parameters. However, some antibodies (e.g., phospho‑specific antibodies) directly sequester phospho‑sites and can be represented as an additional inhibitory term on phosphorylation rates. Thus the extension can leverage existing phosphorylation dynamics but requires new state variables for antibody‑antigen complexes and new input mapping. It remains outside the core but can interface through targeted parameter modifications.
+
+## Current PhosKinTime assets this can reuse
+
+- **Phosphorylation rates and kinase activity outputs:** predicted phosphorylation rates per site[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation) and kinase activities[\[9\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) can be targeted by antibodies.
+- **Residue-level mapping:** data preprocessing produces mappings of kinase‑phosphorylation interactions and gene identifiers[\[11\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,mapping%20files%20for%20further%20analysis); this can be extended to map antibody targets.
+- **Sensitivity analysis:** can identify phospho‑sites or proteins whose perturbation significantly alters network behaviour[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=), guiding antibody targeting strategies.
+- **Optimization and simulation engine:** the same ODE solver can incorporate additional binding ODEs; the export and dashboard modules can display perturbed trajectories.
+
+## Required new scientific layer
+
+- **Binding kinetics:** For each antibody/epitope pair, introduce variables for free antibody (`Ab`), free antigen (`Ag`) and bound complex (`AbAg`). Mass‑action kinetics:
+
+$$\frac{dAb}{dt} = - k_{on}Ab \times Ag + k_{off}AbAg - k_{clear}Ab$$
+
+$$\frac{dAg}{dt} = - k_{on}Ab \times Ag + k_{off}AbAg + ...$$
+
+$$\frac{dAbAg}{dt} = k_{on}Ab \times Ag - k_{off}AbAg - k_{clear\_ complex}AbAg$$
+
+`Ag` corresponds to the concentration of a specific protein or phospho‑site predicted by PhosKinTime; `Ab` is the antibody concentration input; `k_on`, `k_off`, `k_clear` are binding and clearance rates.
+
+- **Effect on signalling:** Bound antigen is considered inactive. For kinase targets, effective kinase activity multiplier is reduced; for phospho‑site targets, forward phosphorylation rate scaling `kappa_{g,s}` could be modulated.
+
+- **Epitope mapping:** A mapping file linking antibody names to gene symbols, phosphorylation site positions or domains (e.g., Y1234) is required. For antibodies targeting conformational epitopes, integration with the structural extension (Section 4) may be necessary.
+
+- **Antibody clearance:** Antibody pharmacokinetics may be modelled similarly to the PK extension (Section 1) but often with longer half‑life; optional compartments can be added.
+
+## Realistic inputs
+
+| Filename | Required columns | Optional columns | Units | Source | Notes |
+|-----------------------|-----------------------------------------------------------------------------------|-------------------------|-----------------|-------------------------------|-----------------------------------------------------------------------------------------------------------|
+| `antibody_dose.csv` | `time`, `dose_concentration` | `isotype` | hours, µM | User-provided dosing protocol | Defines when antibodies are administered and at what concentration. |
+| `antibody_params.csv` | `antibody`, `target_type`, `target_id`, `k_on`, `k_off`, `k_clear`, `effect_type` | `Kd`, `stoichiometry` | 1/(µM hr), hr⁻¹ | Literature, BindingDB | Binding kinetics per antibody-target pair; `effect_type` describes how binding modifies model parameters. |
+| `epitope_mapping.csv` | `antibody`, `gene`, `psite_position`, `residue`, `affinity_score` | `structure_id` | | IEDB, PDB | Maps antibodies to specific proteins or phospho‑sites; may include structural annotations. |
+| `antibody_pk.csv` | `k_abs`, `k_el`, `volume` | | hr⁻¹, hr⁻¹, L | Pharmacokinetic studies | Optional PK model for antibody clearance. |
+| `antibody_data.csv` | `time`, `antigen_activity` | `complex_concentration` | hours | Experimental data | For fitting binding parameters and validating model predictions. |
+
+## Realistic input sources
+
+- **IEDB and ImmPort:** epitope and monoclonal antibody binding data.
+- **BindingDB and DrugBank:** affinity constants (`Kd`), association/dissociation rates for antibody‑protein pairs.
+- **Therapeutic antibody databases (e.g., Thera‑SAbDab):** sequences, isotypes and kinetic parameters.
+- **Bioinformatics tools:** predicted epitopes using Bepipred, DiscoTope; structural mapping via PDB or AlphaFold; cross‑reference with `processing/map.py` outputs[\[11\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,mapping%20files%20for%20further%20analysis).
+
+## Outputs
+
+| Output file | Description |
+|-----------------------------|--------------------------------------------------------------------------------------------|
+| `antibody_binding.csv` | Simulated free and bound antibody/antigen concentrations over time. |
+| `antibody_effects.csv` | Effective modifications to kinase activities or phosphorylation rate constants over time. |
+| `antibody_trajectories.csv` | Predicted mRNA, protein and phospho trajectories under antibody perturbation. |
+| `antibody_residuals.csv` | Differences between measured and predicted antigen activities and phosphorylation signals. |
+| `antibody_sensitivity.csv` | Sensitivity of binding parameters (e.g., `k_on`, `k_off`) on signalling outcomes. |
+| `antibody_report.json` | Metadata summarizing binding parameters, epitope mapping, and provenance. |
+| `antibody_plots/` | Plots of antibody concentration, binding kinetics, and perturbed trajectories. |
+
+## Proposed repository structure
+
+ extensions/
+ antibody/
+ __init__.py
+ binding.py # defines mass‑action binding kinetics and antibody PK
+ coupling.py # maps bound complexes to PhosKinTime parameter modifications
+ simulate.py # integrates binding ODEs with network model
+ fit.py # fits binding parameters to experimental data
+ export.py # writes outputs and plots
+ inputs.py # validates antibody-related tables
+ config.py # configuration schema
+ cli.py
+ tests/
+ test_binding.py
+ test_coupling.py
+
+## Configuration design
+
+Add `[antibody]` to `config.toml`:
+
+ [antibody]
+ dose_schedule = "data/antibody_dose.csv"
+ params = "data/antibody_params.csv"
+ epitopes = "data/epitope_mapping.csv"
+ ab_pk = "data/antibody_pk.csv" # optional
+ fit = false
+ outdir = "results/antibody/"
+
+This minimal section points to the input files; optional flags can enable parameter fitting or PK modelling.
+
+## CLI design
+
+- **Python module:** `phoskintime.extensions.antibody.cli`.
+- **Command:**
+
+
+- python -m phoskintime antibody --conf config.toml [--fit] [--simulate]
+
+
+- **Pixi task:** add an `antibody` task with the same command.
+- Flags: `--fit` to estimate `k_on`, `k_off` and clearance; `--simulate` to run with fixed parameters; `--plot` to generate binding and trajectory plots.
+- Output directories: `//` with subfolders for binding, trajectories and plots.
+
+## Minimal viable implementation
+
+- **Inputs:** Single antibody targeting one phospho‑site; known `k_on` and `k_off`; a simple dose schedule.
+- **Algorithm:** Simulate binding kinetics using ordinary differential equations; compute effective reduction in phosphorylation rate scaling; run `networkmodel` with modified `kappa_{g,s}`; output perturbed trajectories.
+- **Outputs:** Antibody binding curve; reduction in target phosphorylation; predicted time courses of protein/phospho levels; simple plots.
+- **Tests:** unit test for binding ODE; integration test verifying that increased antibody concentration reduces phosphorylation output; test that mapping between epitope and parameter works.
+
+## Full implementation roadmap
+
+- **v0.1:** Implement binding kinetics and simple inhibitory coupling; create CLI and configuration; provide tests.
+- **v0.2:** Support multiple antibodies and multiple targets; incorporate stoichiometry and crosslinking effects; optional PK model for antibody clearance.
+- **v0.3:** Enable fitting of binding parameters using measured binding or functional data; integrate with sensitivity analysis to prioritise targets.
+- **v0.4:** Incorporate structural epitope mapping from Section 4; include antibody isotype effects (e.g., Fc‑mediated clearance); add example notebooks.
+- **v1.0:** Provide a validated extension with documentation and tested on therapeutic antibody perturbation datasets.
+
+## Validation strategy
+
+- **Unit tests:** check mass‑action binding solutions; ensure that the mapping of bound complex reduces the corresponding kinetic parameter.
+- **Integration tests:** simulate a scenario with known binding parameters and verify expected reduction in phosphorylation; ensure that adding more antibodies intensifies inhibition.
+- **Synthetic data tests:** generate synthetic binding and signalling data; fit `k_on`, `k_off` and verify recovery.
+- **Biological sanity checks:** compare predicted binding half‑life and occupancy with literature; ensure that the model does not predict antibody levels beyond plausible physiological range.
+- **External benchmarks:** use published antibody perturbation proteomics datasets to validate predicted effects on phosphorylation and downstream transcription.
+
+## Risks and failure modes
+
+- **Mapping uncertainty:** epitope mapping may be uncertain; mismapping an antibody to the wrong site could mislead predictions.
+- **Data limitations:** kinetic constants are often unavailable for specific antibodies and conditions; may require assumptions.
+- **In vivo complexity:** antibody effects include immune effector functions (e.g., ADCC) not modelled here; ignoring them may oversimplify biological outcomes.
+- **Non‑identifiability:** `k_on` and `k_off` may be correlated, leading to poorly constrained fits.
+- **Numerical stiffness:** adding binding kinetics may increase stiffness, requiring careful solver settings.
+
+## What not to do
+
+- Do **not** claim to design antibodies or predict epitope sequences; rely on external epitope mapping and kinetics.
+- Do **not** assume that antibody binding always results in complete inhibition; allow partial occupancy.
+- Do **not** ignore clearance of antibodies or complexes when modelling long time courses.
+- Do **not** modify core ODEs; implement binding through additional compartments.
+- Do **not** embed proprietary antibody sequences.
+
+## Recommended priority
+
+**Later / downstream integration.** Although antibody perturbations are biologically relevant, the complexity of binding kinetics and scarcity of high‑quality time‑course data make this a lower priority. It should be developed after PK‑PD and structural annotation extensions are established.
+
+# 4. Structural Bioinformatics Extension
+
+## Executive summary
+
+This extension provides structural annotations for proteins and phosphorylation sites represented in PhosKinTime. It does not alter the dynamic model but enriches outputs with domain, secondary structure, solvent accessibility and homology information. These annotations can aid interpretation of fitted parameters and prioritise targets for experimental validation or MD simulations. The extension maps each protein (gene) to a UniProt accession, retrieves or predicts a structure (PDB or AlphaFold), and uses tools such as DSSP or FreeSASA to compute per‑residue features. It also maps phosphorylation sites to 3D coordinates and determines whether they lie in ordered domains, disordered regions or interfaces. The extension does **not** perform structural modelling or design; it annotates existing structures.
+
+## Why this is or is not close to the current framework
+
+- **Classification:** downstream annotation layer.
+- **Justification:** The current model does not use structural information; outputs are purely temporal and quantitative. Structural annotation does not change the ODE system but adds metadata and derived metrics. It can be implemented as a separate post‑processing step that reads existing outputs (phosphorylation rates, sensitivities, mapping tables) and enriches them. Therefore it is conceptually decoupled and safe to implement without altering core models.
+
+## Current PhosKinTime assets this can reuse
+
+- **Mapped gene and phosphorylation site identifiers:** The preprocessing mapping step produces tables `mapping.csv` and `nodes.csv` linking kinases, genes and phospho‑sites[\[12\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,).
+- **Phosphorylation rate outputs:** `export_S_rates` summarises phosphorylation rates by protein and site[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation). These can be annotated with structural features.
+- **Sensitivity rankings:** sensitivity analysis identifies influential parameters or sites[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=); structural annotation can prioritise high‑impact sites.
+- **Configuration system:** a new section can be added without affecting existing modules. Export utilities and dashboard can be extended to include structural columns.
+
+## Required new scientific layer
+
+- **Sequence-to-structure mapping:** map each gene symbol to a UniProt accession using external databases (e.g., MyGeneInfo) and retrieve structures:
+
+- If a resolved structure exists in the Protein Data Bank, download the PDB file.
+
+- Otherwise, retrieve an AlphaFold model from AlphaFold DB or EBI PDB. Use the highest‑confidence model for the species of interest.
+
+- **Phosphosite mapping:** use the residue number and amino acid from the phosphorylation site (from `psite_position` and `residue` fields) to locate the corresponding residue in the structure. Consider isoform numbering; if mapping fails, flag the site for manual curation.
+
+- **Feature computation:** compute per‑residue annotations:
+
+- Secondary structure (helix, sheet, loop) using DSSP.
+
+- Solvent accessible surface area (SASA) using FreeSASA or DSSP output.
+
+- Domain and motif annotations via Pfam/InterPro.
+
+- Disorder prediction using tools like IUPred or by absence of structure.
+
+- **Integrative summary:** join computed features with phosphorylation rate and sensitivity tables. Provide summary statistics (e.g., average SASA for high‑sensitivity sites). Optionally export PyMOL session scripts to visualise highlighted sites.
+
+## Realistic inputs
+
+| Filename | Required columns | Optional columns | Units | Source | Notes |
+|-------------------------|------------------------------------------------------------------------------|------------------|-------|-----------------------------|-----------------------------------------------------------------------|
+| `structure_sources.csv` | `gene`, `uniprot_id`, `preferred_pdb_id` | `isoform` | | UniProt, MyGeneInfo | Map gene to structure; `preferred_pdb_id` can be blank for AlphaFold. |
+| `psite_mapping.csv` | `gene`, `residue`, `position`, `structure_chain`, `structure_residue_number` | | | Generated by mapping script | Connects phospho‑sites to structure residues. |
+| `features_config.csv` | `feature`, `tool`, `parameters` | | | User configuration | Lists which structural features to compute and tool settings. |
+| `structures/` | downloaded PDB or mmCIF files | | | RCSB PDB, AlphaFold DB | Local storage for structures. |
+
+## Realistic input sources
+
+- **UniProt and MyGeneInfo:** mapping from gene symbol to UniProt accession and isoform information.
+- **Protein Data Bank (PDB):** resolved protein structures.
+- **AlphaFold DB:** predicted structures when no PDB exists.
+- **Pfam / InterPro:** domain and motif annotations.
+- **DSSP / FreeSASA:** tools for secondary structure and SASA computation.
+- **Disorder prediction servers:** IUPred, MobiDB-lite for intrinsic disorder.
+
+## Outputs
+
+| Output file | Description |
+|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `structure_annotations.csv` | Per‑residue table with secondary structure, SASA, domain and disorder information. |
+| `psite_structural.csv` | Phosphorylation sites annotated with structural features and mapped coordinates. |
+| `site_prioritization.csv` | Ranked list of phospho‑sites combining sensitivity score, phosphorylation rate[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation) and structural features (e.g., surface exposure). |
+| `protein_models/` | Downloaded PDB/mmCIF or AlphaFold structures, renumbered and trimmed to match sequences. |
+| `pymol_sessions/` | PyMOL or ChimeraX session files highlighting high‑impact sites. |
+| `structural_report.json` | Metadata summarizing methods, data sources and versioning. |
+
+## Proposed repository structure
+
+ extensions/
+ structural/
+ __init__.py
+ fetch_structures.py # download PDB/AlphaFold models given UniProt IDs
+ map_sites.py # map phospho positions to structure residue numbers
+ compute_features.py # run DSSP, FreeSASA and disorder predictors
+ prioritize.py # rank sites combining structural and dynamic metrics
+ export.py # write annotation tables and PyMOL scripts
+ config.py # configuration schema
+ cli.py
+ tests/
+ test_mapping.py
+ test_feature_computation.py
+
+The structural extension is separated from dynamic modelling; it reads existing outputs and writes annotation files.
+
+## Configuration design
+
+Add `[structural]` to `config.toml`:
+
+ [structural]
+ # Path to mapping of genes to UniProt accessions
+ source_map = "data/structure_sources.csv"
+ # Directory to store downloaded structures
+ structure_dir = "data/structures/"
+ # Feature configuration file
+ features = "data/features_config.csv"
+ # Whether to compute disorder predictions
+ compute_disorder = true
+ # Output directory
+ outdir = "results/structural/"
+
+## CLI design
+
+- **Python module:** `phoskintime.extensions.structural.cli`.
+- **Command:**
+
+
+- python -m phoskintime structural --conf config.toml [--fetch] [--annotate] [--prioritize]
+
+
+- **Pixi task:** add `structural` command with the same options.
+- Flags: `--fetch` downloads structures; `--annotate` maps sites and computes features; `--prioritize` ranks sites; `--plot` could generate structural summaries.
+- Output directories follow `//` for gene‑specific annotations.
+
+## Minimal viable implementation
+
+- **Inputs:** Mapping file for a small set of genes; list of phospho‑sites; simple feature configuration (secondary structure and SASA only).
+- **Algorithm:** Fetch structures from PDB/AlphaFold; map phospho‑site positions to residue numbers; run DSSP to compute secondary structure and SASA; join with phosphorylation rate table; export annotated table.
+- **Outputs:** `psite_structural.csv` with columns for residue, secondary structure and SASA; simple ranking combining high SASA and high phosphorylation rate.
+- **Tests:** unit tests for mapping (correct residue numbers), feature computation (expected output length) and prioritisation (ties broken consistently).
+
+## Full implementation roadmap
+
+- **v0.1:** Implement structure fetching and site mapping; compute secondary structure and SASA; basic ranking; include tests.
+- **v0.2:** Add disorder prediction and domain/motif annotations from Pfam/InterPro; support isoforms; compute solvent exposure categories (buried vs. exposed).
+- **v0.3:** Integrate sensitivity scores[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) and phosphorylation rate rankings[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation); export combined ranking; generate PyMOL sessions; include interactive dashboards.
+- **v0.4:** Provide precomputed annotations for common organisms; support alternative structural sources (e.g., Swiss‑Model); implement caching and update mechanisms.
+- **v1.0:** Stable release with documentation, validated mapping on test datasets and reproducible examples.
+
+## Validation strategy
+
+- **Unit tests:** verify that residue mapping correctly handles numbering offsets and missing residues; check that feature computation tools are invoked and outputs match expected dimensions.
+- **Integration tests:** run the full pipeline on a test dataset (e.g., a small set of proteins) and check that outputs contain expected columns and values; verify that high‑SASA sites are correctly identified.
+- **Cross‑validation:** compare computed secondary structure and SASA with annotations from existing resources (e.g., DSSP or PDBsum); confirm mapping accuracy using manual inspection for a subset.
+- **Regression tests:** ensure that adding new features or updating databases does not break existing functionality.
+
+## Risks and failure modes
+
+- **Mapping errors:** mismatches between gene isoforms, sequence numbering and structure numbering can lead to incorrect site annotations.
+- **Data availability:** some proteins may lack resolved structures; AlphaFold predictions may have low confidence in certain regions.
+- **Tool dependencies:** DSSP, FreeSASA and disorder predictors must be installed; version differences can yield different results.
+- **Computational cost:** large numbers of proteins/sites could slow computation; caching and parallelisation may be needed.
+- **Overinterpretation:** structural annotation does not confirm functional importance; high SASA does not guarantee accessibility to kinases or antibodies.
+
+## What not to do
+
+- Do **not** attempt de novo structure prediction or MD simulations; rely on existing structures or AlphaFold models.
+- Do **not** override core outputs; structural annotation is supplementary.
+- Do **not** claim that structural features alone determine phosphorylation or druggability; use them in combination with dynamic metrics.
+- Do **not** embed large structural files in the repository; download and cache them externally.
+- Do **not** mix isoforms without explicit mapping.
+
+## Recommended priority
+
+**First.** Structural annotation is independent of dynamic modelling and provides immediate value by contextualising fitted parameters. It requires no changes to core ODE models and can be implemented as a post‑processing pipeline using publicly available tools and databases.
+
+# 5. Molecular Dynamics Simulation Prioritization Extension
+
+## Executive summary
+
+This extension prioritises proteins or phosphorylation sites for detailed molecular dynamics (MD) simulations based on PhosKinTime outputs and structural annotations. It does **not** perform MD simulations itself; rather, it generates ranking tables, prepares input files and optionally runs lightweight energy minimisations to check structural feasibility. The goal is to select a manageable subset of sites or protein complexes that show high phosphorylation flux, sensitivity or drug/antibody targeting potential for further in silico or in vitro study. The extension leverages structural annotations from Section 4 and dynamic metrics from PhosKinTime to produce an integrated prioritisation.
+
+## Why this is or is not close to the current framework
+
+- **Classification:** downstream annotation layer / external pipeline integration.
+- **Justification:** MD simulations operate on atomistic timescales and are outside the scope of the ODE-based model. However, the extension can use dynamic outputs (phosphorylation rates, sensitivities)[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation)[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) and structural annotations to guide MD simulation planning. It does not modify the dynamic model; therefore it is a downstream integration aimed at bridging PhosKinTime with molecular simulation pipelines.
+
+## Current PhosKinTime assets this can reuse
+
+- **Phosphorylation rate and kinase activity outputs:** used to identify sites with high flux[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation).
+- **Sensitivity analysis:** identifies parameters whose perturbation most affects outcomes[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=); sites with high sensitivity may be prioritised.
+- **Structural annotations:** generated by the structural extension; provide PDB/AlphaFold structures and per‑residue features.
+- **Mapping tables:** `mapping.csv` and `psite_structural.csv` link dynamic entities to structure residues[\[12\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,).
+- **Configuration and export utilities:** to read dynamic outputs and write ranking tables.
+
+## Required new scientific layer
+
+- **Ranking criteria:** define a composite score for each phospho‑site or protein combining:
+
+- Normalised phosphorylation rate magnitude[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation).
+
+- Sensitivity score[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=).
+
+- Structural accessibility (e.g., high SASA) and disorder from structural annotations.
+
+- Potential druggability or antibody targeting (if mapped in other extensions). Weights for each criterion can be user‑defined.
+
+- **MD readiness assessment:** check whether the structure covers the phospho‑site (no missing residues), whether the environment is conducive to MD (no unresolved loops) and whether post‑translational modifications (phosphorylation) can be parameterised with available force fields.
+
+- **Preparation of simulation files:** generate coordinate (PDB) and topology (e.g., CHARMM or AMBER) inputs for the selected sites or proteins. Use existing tools (e.g., BioPandas or MDAnalysis) to extract segments and optionally apply phosphorylation modifications using libraries like ParmEd. Provide instructions or scripts for running MD in GROMACS or OpenMM, but do not run them within PhosKinTime.
+
+- **Integration with docking or coarse‑grained simulation pipelines (optional):** prepare mutated or modified structures for further analysis.
+
+## Realistic inputs
+
+| Filename | Required columns | Optional columns | Units | Source | Notes |
+|---------------------------------|---------------------------------------------------------------------------|---------------------------------------|-------|-----------------------------|--------------------------------------------------------------------------------|
+| `md_prioritization_params.toml` | `weights.rate`, `weights.sensitivity`, `weights.sasa`, `weights.disorder` | `weights.druggability`, `max_targets` | | User-defined | Defines how to weigh different criteria and how many targets to select. |
+| `psite_structural.csv` | `gene`, `psite`, `rate`, `sensitivity`, `sasa`, `disorder` | `druggability_score` | | From structural extension | Contains all metrics needed for ranking. |
+| `forcefield_params.csv` | `residue`, `phosphorylated_topology` | | | Force-field parameter files | To ensure that selected phospho‑sites are supported by chosen MD force fields. |
+| `md_templates/` | Predefined MD system templates (e.g., water box, ion parameters) | | | Provided by user | Templates for MD simulation preparation. |
+
+## Realistic input sources
+
+- **Outputs from PhosKinTime:** phosphorylation rates and sensitivity analyses[\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation)[\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=).
+- **Structural extension outputs:** `psite_structural.csv` with SASA and disorder scores.
+- **Force field libraries:** AMBER, CHARMM, GROMOS; parameters for phosphorylated residues.
+- **MD tools:** MDAnalysis, BioPandas, ParmEd for structure manipulation; GROMACS or OpenMM for simulation templates.
+
+## Outputs
+
+| Output file | Description |
+|------------------------|--------------------------------------------------------------------------------------------------|
+| `md_priority_list.csv` | Ranked list of sites or proteins with composite scores and selection rationale. |
+| `md_ready_structures/` | Directory containing extracted PDB files of selected sites/proteins prepared for MD. |
+| `md_input_scripts/` | Template simulation scripts (e.g., GROMACS `.mdp` files) with system setup parameters. |
+| `md_report.json` | Metadata summarising ranking criteria, selected targets and provenance. |
+| `md_plots/` | Bar plots or scatter plots of composite scores vs. criteria weights to visualise prioritisation. |
+
+## Proposed repository structure
+
+ extensions/
+ md_prioritization/
+ __init__.py
+ rank.py # compute composite scores and produce ranking
+ prepare.py # generate MD-ready structure files
+ export.py # write rankings, scripts and plots
+ config.py # configuration schema
+ cli.py
+ tests/
+ test_rank.py
+ test_prepare.py
+
+This extension depends on outputs from the structural extension; it should be placed under `extensions` and can be run separately.
+
+## Configuration design
+
+Add `[md_prioritization]` to `config.toml` or create a separate TOML file:
+
+ [md_prioritization]
+ # Weights for ranking criteria (must sum to 1)
+ weights = {rate = 0.4, sensitivity = 0.3, sasa = 0.2, disorder = 0.1}
+ # Maximum number of targets to select
+ max_targets = 10
+ # Force-field parameter file for phosphorylated residues
+ forcefield_params = "data/forcefield_params.csv"
+ # MD template directory
+ templates = "data/md_templates/"
+ # Output directory
+ outdir = "results/md_prioritization/"
+
+Users can adjust weights depending on the research question; the extension will normalise them.
+
+## CLI design
+
+- **Python module:** `phoskintime.extensions.md_prioritization.cli`.
+- **Command:**
+
+
+- python -m phoskintime md-prioritization --conf config.toml [--rank] [--prepare]
+
+
+- **Pixi task:** add `md-prioritization` task with the same command.
+- Flags: `--rank` generates the ranking list; `--prepare` creates MD-ready files for the top `max_targets`; `--plot` generates visualisations.
+- Output directories follow `/` with subdirectories `rankings`, `structures`, `scripts` and `plots`.
+
+## Minimal viable implementation
+
+- **Inputs:** `psite_structural.csv` with rate and sensitivity; user‑defined weights; basic force‑field parameter list for phosphorylated serine and tyrosine.
+- **Algorithm:** Normalise each criterion; compute a weighted sum to obtain composite scores; sort sites; select top N; extract the corresponding residues and their structural environment (e.g., ±10 residues) using MDAnalysis; save as PDB files; generate placeholder MD simulation scripts referencing those PDBs.
+- **Outputs:** `md_priority_list.csv` with ranking and scores; `md_ready_structures/` with extracted PDB segments; simple bar plot of scores.
+- **Tests:** unit tests verifying scoring and ranking; integration test ensuring that extracted PDB segments contain the correct residues; test that missing force‑field parameters raise warnings.
+
+## Full implementation roadmap
+
+- **v0.1:** Implement ranking based on dynamic and structural metrics; prepare simple PDB extracts; generate template scripts; include tests.
+- **v0.2:** Support ranking at protein level and include druggability/antibody targeting scores if available; add interactive plots; implement user-defined scoring functions.
+- **v0.3:** Automate checking of force‑field compatibility; add optional energy minimisation using OpenMM; export results in formats ready for MD packages.
+- **v0.4:** Integrate with docking or coarse‑grained simulation pipelines; provide example notebooks and cross‑validation on known benchmark proteins.
+- **v1.0:** Mature extension with documentation and published use cases.
+
+## Validation strategy
+
+- **Unit tests:** verify correct computation of composite scores; ensure that ranking is reproducible and respects `max_targets` constraint.
+- **Integration tests:** run the full pipeline on a small dataset; confirm that extracted PDB segments correspond to the intended residues and that template scripts reference the correct files.
+- **Cross‑validation:** compare rankings to known importance of phospho‑sites from literature; confirm that high‑ranked sites often correspond to functional hotspots.
+- **Regression tests:** ensure that modifications (e.g., weight changes) produce expected differences but not unintended side effects.
+
+## Risks and failure modes
+
+- **Subjective weighting:** composite scores depend on chosen weights; different users may prioritise different criteria.
+- **Force-field limitations:** some modifications may not be supported; manual parameterisation may be needed.
+- **Structural coverage:** structures may not include the region of interest, leading to ranking based on incomplete information.
+- **Overinterpretation:** ranking does not guarantee functional importance; experimental validation is required.
+- **Complex pipelines:** preparing MD-ready systems may require manual inspection; automation can fail on edge cases (e.g., hetero-oligomers).
+
+## What not to do
+
+- Do **not** run full-scale MD simulations within the extension; focus on preparing inputs and rankings.
+- Do **not** claim that the top-ranked sites will always yield meaningful MD insights; they are suggestions based on available metrics.
+- Do **not** assign equal weight to all criteria without justification; encourage users to tailor weights.
+- Do **not** embed large MD templates or parameter files in the repository; provide links or instructions.
+- Do **not** alter core PhosKinTime ODE models.
+
+## Recommended priority
+
+**Only as a downstream integration.** This extension relies on structural annotations and dynamic metrics; it does not feed back into the modelling. It should be developed after structural annotation and PK/PD modules are in place. Its value lies in guiding external MD studies rather than enhancing PhosKinTime core functionality.
+
+# Recommended Implementation Order
+
+1. **Structural Bioinformatics Extension (Section 4)** — This is the most straightforward to implement because it operates as a separate post‑processing pipeline. It does not modify the core ODE model and relies on well‑established tools and publicly available databases. Structural annotation immediately enhances interpretability and enables downstream prioritisation of sites without requiring new experimental data.
+
+2. **Drug Dose‑Response / PK‑PD / QSP Extension (Section 1)** — Extending the model to include drug effects is highly relevant for pharmacological studies. The conceptual and implementation complexity is moderate; it leverages existing ODE infrastructure and parameter outputs. Implementing PK/PD will open the door to modelling other perturbations and is grounded in widely used PK models.
+
+3. **mRNA Vaccine / Antigen‑Expression / Immune‑Response Extension (Section 2)** — This extension requires adding new biological layers (exogenous mRNA, antigen translation and cytokine signalling) and depends on external kinetic data. While feasible, it introduces more assumptions and new state variables. It should follow once PK/PD modelling and structural annotation are stable.
+
+4. **Antibody Perturbation / Antibody‑Response Extension (Section 3)** — Modelling antibody binding kinetics is biologically interesting but demands detailed binding parameters and epitope mapping. Data scarcity and mapping uncertainties make this a lower priority. It can be implemented as a separate module after PK/PD and structural modules are mature.
+
+5. **Molecular Dynamics Simulation Prioritization Extension (Section 5)** — This is purely a downstream integration for ranking and preparing MD simulations. It depends on structural annotations and dynamic metrics, offering no direct feedback to the core model. It should be considered only after earlier extensions are in place and when there is specific interest in atomistic simulations.
+
+[\[1\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=,factor%20scaling%20parameter) [\[2\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=predicted%20change%20should%20be%20interpreted,not%20as%20a%20standalone%20correlation) [\[3\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%60%60%60text%20TIME_POINTS_PROTEIN%20,phospho%20fitted%20trajectories) [\[4\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) [\[5\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) [\[9\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=) [\[10\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/networkmodel/INTERPRETATION.md#:~:text=%23%23%20Phosphorylation) raw.githubusercontent.com
+
+
+
+[\[6\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/kinopt/README.md#:~:text=%2A%2Akinopt%2A%2A%20provides%20an%20end,for) raw.githubusercontent.com
+
+
+
+[\[7\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,120%2C%20240%2C%20480%2C%20960) [\[8\]](https://raw.githubusercontent.com/bibymaths/phoskintime/global/config.toml#:~:text=,0) raw.githubusercontent.com
+
+
+
+[\[11\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,mapping%20files%20for%20further%20analysis) [\[12\]](https://raw.githubusercontent.com/bibymaths/phoskintime/main/processing/README.md#:~:text=,) raw.githubusercontent.com
+
+
diff --git a/docs/assets/coverage.svg b/docs/assets/coverage.svg
index 012a849..ffd257b 100644
--- a/docs/assets/coverage.svg
+++ b/docs/assets/coverage.svg
@@ -15,7 +15,7 @@
coverage
coverage
- 70%
- 70%
+ 71%
+ 71%
diff --git a/docs/assets/images/phoskintime_workflow.png b/docs/assets/images/phoskintime_workflow.png
new file mode 100644
index 0000000..f2bb2ab
Binary files /dev/null and b/docs/assets/images/phoskintime_workflow.png differ
diff --git a/docs/dashboard_build_plan.md b/docs/dashboard_build_plan.md
new file mode 100644
index 0000000..91c6522
--- /dev/null
+++ b/docs/dashboard_build_plan.md
@@ -0,0 +1,396 @@
+# PhosKinTime No‑Code Frontend Audit and Build Plan
+
+This document summarizes the current state of the **PhosKinTime** repository and proposes a structured approach for building a unified no‑code dashboard. It is intended as a planning aid; **no frontend implementation is included**. The audit draws from the repository contents and documentation available as of 9 June 2026.
+
+## 1. Repository Audit Summary
+
+### 1.1 Overview of the repository
+
+PhosKinTime is organised as a Python package with several subpackages, configuration files, documentation, Streamlit applications and post‑processing scripts. The top‑level directories and their functions are summarised below:
+
+- `kinopt` – optimisation of kinase‑phosphorylation interactions. Contains two flavours (`local` and `evol`), a `fitanalysis` module for analysing optimisation performance and an `optimality` subpackage. The `local` flavour has a `__main__.py` that implements a CLI using `argparse`; it parses bounds and loss settings and orchestrates data loading, multistart optimisation and result export[\[1\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md).
+- `tfopt` – optimisation of transcription factor–mRNA relations. Similar structure to `kinopt` with `local` and `evol` flavours and a `fitanalysis` submodule. The CLI for the `local` flavour reads bounds and loss type from `config.toml` and runs a multistart optimiser before exporting results.
+- `protwise` – contains mechanistic ODE models and estimation routines for protein dynamics. It includes modules for parameter estimation (`paramest`), plotting, steady state calculations and a `runner` submodule that exposes an entry point (`protwise.runner.main`) used by the config CLI and pixi tasks.
+- `networkmodel` – the integrated global model built on JAX. It includes routines for building matrices, running optimisation (`runner.py`), Bayesian inference, sensitivity analysis and exporting results. The module also provides a Streamlit dashboard (`dashboard_app.py`) and a bundle utility (`dashboard_bundle.py`) for serialising optimisation results. The CLI in `runner.py` accepts numerous arguments (paths to network files, data files, regularisation parameters, solver choice, number of generations, etc.) and writes output to an `--output-dir` with subfolders for optimisation, profiles, posterior samples, plots and a pickled dashboard bundle[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py)[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md).
+- `common` – shared utilities, Frechét distance computation and helpers used across modules.
+- `processing` – preprocessing routines (currently a `cleanup` module) invoked by the `config/cli.py` `prep` command.
+- `config` – default configuration and command‑line shortcuts. The file `config/cli.py` defines a Typer app with commands `prep`, `tfopt`, `kinopt`, `model`, `networkmodel`, `clean` and `all`. Each command delegates to the appropriate module via Python `-m` and passes `--conf` options if provided[\[1\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). The `config_loader.py` reads TOML/YAML configuration files.
+- `app` – Streamlit dashboards for interactive exploration of TF and kinase optimisation results. `app/tfopt.py` provides an upload‑based dashboard for TF optimisation and network visualisation; it recomputes model‑consistent signal flow, generates numerous plots (latent activity, dominance, knockout effects, scatter and bar charts) and renders networks via **gravis**. `app/kinopt.py` offers similar functionality for kinase optimisation[\[1\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). These apps are independent of the package CLI and expect Excel files with specific sheet names.
+- `scripts` – stand‑alone analysis and visualisation scripts intended to be run after the main pipeline. The documentation lists their purposes and default arguments[\[4\]](chrome://newtab/)[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). Scripts include `analyze_tf_kin_counts.py`, `compare_mechanisms.py` (Streamlit app), `curve_similarity.py`, `export_subnetworks.py`, `find_protein_accumulators.py`, `mechanistic_insights.py`, `temporal_sensitivity.py`, and others.
+- `docs` – MkDocs documentation under `docs/Documentation`. Key pages include a script reference[\[4\]](chrome://newtab/)[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) and dashboard usage guide[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py)[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md).
+- `tests` – pytest suite covering configuration loading, notebook execution, inference routines and dashboard behaviour. The tests assert that optimisation routines save expected CSVs, NPZ files and plots in specific subdirectories (`optimization/`, `profiles/`, `posterior/`, `plots/`) and that the dashboard can load inference outputs.
+
+### 1.2 Major functional areas
+
+| Area | Description |
+|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Scientific model modules** | `kinopt`, `tfopt`, `protwise`, `networkmodel`, plus utilities in `common` and `processing`. These contain the core optimisation, simulation, parameter estimation and ODE models. |
+| **CLI entry points** | `config/cli.py` provides Typer‑based commands; `kinopt.local/__main__.py`, `tfopt.local/__main__.py`, `networkmodel/runner.py` implement `argparse` CLIs; `protwise/runner/main.py` is called via pixi task. |
+| **Pixi tasks** | `pixi.toml` defines tasks for running tests, building docs, executing optimisations (`kinopt-local`, `kinopt-evol`, `tfopt-local`, `tfopt-evol`), preprocessing, running the global model (`networkmodel`), and launching the Streamlit dashboard (`network-dashboard`). |
+| **Scripts** | Post‑processing utilities in `scripts/` used after optimisation. They analyse beta counts, compute Fréchet distances, export subnetworks, identify accumulator proteins, summarise mechanistic insights and perform temporal sensitivity analysis[\[5\]\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). |
+| **Dashboards / Streamlit apps** | Streamlit apps in `app/` for TF and kinase optimisation readouts; `networkmodel/dashboard_app.py` for the global model; `run_dashboard.py` for launching the global dashboard; `scripts/compare_mechanisms.py` for mechanistic comparison. |
+| **Result viewers** | `networkmodel/dashboard_app.py` loads a pickled bundle and various CSVs/plots to display scalar objective values, convergence history, predicted vs. observed timeseries and residuals[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py). The TF/kinase dashboards compute and display analyses directly from uploaded Excel files. |
+| **Tests** | The pytest suite checks CLI correctness, output file saving, parameter projection, inference routines and that the dashboard handles missing files gracefully. Tests also ensure that old dependencies (e.g., `scipy.integrate.solve_ivp`) are not used. |
+| **Documentation** | Extensive documentation covers installation, pipeline overview, dashboards, script usage and developer guidelines. It includes a script reference table and dashboard instructions[\[4\]](chrome://newtab/)[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py). |
+| **Generated outputs / ignored files** | Optimisation routines produce results directories (e.g., `results/kinopt_local_*`, `results/tfopt_local_*`, `results_model_global_*`) containing CSVs, NPZ files, Excel files, plots, reports and pickled bundles. Git ignores notebooks outputs, compiled artefacts and result folders via `.gitignore`. |
+
+## 2. Existing CLI and Pixi Entry Points
+
+The table below summarises each CLI or Pixi task, its source, purpose, inputs and suitability for a unified frontend. Columns labelled “Inputs” list required inputs first and optional inputs afterwards. “Changes needed” notes refactors necessary before frontend integration.
+
+| Command / Pixi task | Source module | Purpose | Required inputs | Optional inputs | Output behaviour | Suitable for frontend? | Changes needed |
+|---------------------------------------|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `kinopt-local` | `kinopt/local/__main__.py` | Runs gradient‑based kinase optimisation with multistart. Parses lower/upper bounds, loss type, scaling, segmentation and optimisation method[\[1\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Data files: `input1.csv` (protein), `input2.csv` (kinase network) via `config.toml`; segmentation points. | Bounds, loss type, missing‑kinase estimation flag, scaling method, segmentation points, optimiser (slsqp/trust‑constr). | Writes Excel `kinopt_results.xlsx`, CSVs and NPZ to `results` directory and copies results to `data/ode` folder. Creates plots and HTML report via `latexit` and organises outputs. | Yes | Expose explicit `--outdir`; include metadata and command provenance; unify logging; remove hard‑coded copying; improve error handling. |
+| `kinopt-evol` | `kinopt/evol/__main__.py` | Evolutionary optimiser for kinase optimisation. Inputs similar to local flavour. | Same as above | Evolutionary parameters (population size, generations etc.). | Writes results into `results` directory; may differ in folder naming. | Yes | Ensure consistent output structure and metadata; expose `--outdir`; unify CLI argument names. |
+| `kinopt-fitanalysis` | `kinopt/fitanalysis/__main__.py` | Post‑hoc analysis of optimisation performance; reads results and generates plots/summary tables. | Results Excel/NPZ files produced by `kinopt` runs. | None. | Produces plots, CSV summaries. | Yes, but better integrated as part of result viewer rather than separate execution. | Standardise output folder; treat as optional analysis step. |
+| `tfopt-local` | `tfopt/local/__main__.py` | Gradient‑based transcription‑factor optimisation. Parses bounds and loss type, loads expression data and PSite information, runs multistart optimiser and exports results. | Data files: `input1.csv` (protein/phospho), `input3.csv` (RNA), `input4.csv` (TF network). | Bounds, loss type. | Writes `tfopt_results.xlsx`, `multistart_summary.csv`, NPZ files, plots and a report in `results` directory. | Yes | Expose `--outdir`; unify metadata and logging; ensure time grid metadata saved. |
+| `tfopt-evol` | `tfopt/evol/__main__.py` | Evolutionary optimiser for TF optimisation. | Same as above. | Evolutionary parameters. | Writes results into `results` directory. | Yes | Same changes as for `kinopt-evol`. |
+| `prep` | `processing/cleanup.py` via `config/cli.py` | Preprocesses raw data into standardised inputs for optimisation. | Raw CSV files (e.g., `input*.csv` in `data/`). | Path to configuration file via `--conf`. | Writes cleaned CSVs into `data/` and logs to `results/logs`. | Yes | Provide explicit output directory; record provenance; validate inputs with user feedback. |
+| `model` | `protwise/runner/main.py` via pixi or Typer | Runs parameter estimation and ODE simulation for protein time‑series given optimised alpha/beta values. | Configuration file via `--conf`; uses defaults from `config.toml`. | None. | Saves predictions, plots, sensitivity indices and reports under `results`. | Yes | Standardise output structure; ensure JSON/CSV results for frontend consumption. |
+| `networkmodel` | `networkmodel/runner.py` | Runs the global optimisation pipeline combining kinases, TFs and ODE simulation. Accepts numerous CLI arguments for network files, data files, regularisation parameters, solver choice and number of generations[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md). | Input networks: `kinase-net`, `tf-net`; data files: `ms`, `rna`, `phospho`; optional prior results from `kinopt` and `tfopt`. | Output directory (`--output-dir`), solver (`--solver`), number of generations, λ regularisation values, flags for hyperparameter scan (`--scan`), sensitivity analysis, profile likelihood and posterior sampling. | Creates `dashboard_bundle.pkl`, `scalar_objective.csv`/`pareto_F.csv`, `convergence_history.csv`, prediction CSVs, optimisation results (`optimization/*`), profiles, posterior samples and numerous plots[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py)[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md). | Yes, but heavy | Require consistent `--outdir`; guarantee writing of metadata and command provenance; decouple long‑running optimisation from UI; improve error messages. |
+| `phoskintime` **/** `phoskintime-all` | `config/cli.py` Typer commands | Wrapper that calls `processing.cleanup`, `tfopt`, `kinopt` and `protwise` sequentially; the `all` command runs preprocessing → `tfopt` → `kinopt` → `model`. | None by default (uses defaults from `config.toml`). | `--conf` for each stage. | Writes results for each submodule into their respective `results` directories. | Yes | Clarify configuration passing; unify output directories; return status information. |
+| `network-dashboard` | `run_dashboard.py` → `networkmodel/dashboard_app.py` | Launches the global Streamlit dashboard to view completed optimisation results[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py). | `--output-dir` pointing to a completed `networkmodel` run. | Streamlit server options (port). | Interactive viewer only; no new outputs. | Not a backend command but a UI component; embed in unified dashboard. | Already a UI component; to be embedded as a panel. |
+| `compare_mechanisms.py` | `scripts/compare_mechanisms.py` | Streamlit app to compare different network mechanisms and global knockout effects[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | None; uses `config.toml` defaults and result files. | None. | Interactive viewer; heavy simulation. | Could be integrated as an advanced analysis tab. | Parameterise inputs; separate heavy computation; modularise. |
+| **Other scripts** | Various files under `scripts/` | Compute PSite statistics, Fréchet distance, export subnetworks, find accumulator proteins, summarise mechanistic insights and perform temporal sensitivity analysis[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | See script reference; some accept CLI flags for input/output directories[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Additional parameters such as sample size (`--samples`), hop distance (`--hops`), etc. | Write CSVs/plots into specified output directories; some scripts simply print results. | Many can be exposed as on‑demand analyses in the frontend. | Refactor to accept explicit input/output paths; return data structures; include metadata. |
+
+## 3. Existing Dashboard and Streamlit App Audit
+
+The repository already contains several Streamlit applications. Their status and integration recommendations are summarised below.
+
+| App file | Purpose & inputs | Outputs/visualisation | Reusability | Integration recommendation |
+|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `networkmodel/dashboard_app.py` | Global model viewer launched via `run_dashboard.py`. Requires `--output-dir` pointing to a completed `networkmodel` run. Uses `dashboard_bundle.pkl` and CSVs such as `scalar_objective.csv`, `convergence_history.csv`, `pred_prot_picked.csv`, `pred_rna_picked.csv` and `pred_phospho_picked.csv`[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py)[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md). | Displays scalar objective scatter, convergence history plot, predicted vs. observed timeseries, image/video galleries and PDF report viewer. When inference outputs exist (`optimization/`, `profiles/`, `posterior/`), additional tables are shown. | Self‑contained and modular; uses helper functions for loading outputs. | **Keep and import** into the unified dashboard. Expose as a component taking a result directory and rendering the appropriate tabs. |
+| `app/tfopt.py` | Dashboard for TF optimisation. Users upload a `tfopt_results.xlsx` file. The app recomputes model‑consistent signal flow and displays analysis tables (latent activity, target dominance, knockout effects) and numerous plots; it also renders networks via gravis and supports knockout previews. | Visualises bar charts, scatter plots, time‑series fits, network graphs and knockout effects. Provides CSV download buttons. | High reusability; computations are performed on the fly from uploaded Excel; caching is used. | **Import as a workflow panel**. Refactor into functions that separate data processing from UI; reuse plotting and table components; integrate network rendering within unified dashboard. |
+| `app/kinopt.py` | Dashboard for kinase optimisation results. Inputs are `kinopt_results.xlsx` with sheets “Observed”, “Estimated”, “Alpha Values” and “Beta Values”. Computes latent kinase activity, control load, breadth vs. load scatter, bound pressure vs. load, dominance distribution and knockout effects; renders network graphs via gravis; provides CSV downloads. | Similar to TF dashboard: multiple interactive plots and tables. | Reusable with minor refactoring. | **Import as a workflow panel**. Separate computation into helper functions; expose parameter widgets; integrate network rendering into unified dashboard. |
+| `scripts/compare_mechanisms.py` | Streamlit app comparing network mechanisms across different model topologies and showing global knockout effects[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). Uses `networkmodel.config` defaults to locate networks and data; loads optimisation results. | Renders protein/TF networks, computes proxy mapping for orphan transcription factors, filters phospho data mechanistically, simulates until steady state and plots results; depends on heavy libraries (`gravis`, `networkx`, `imageio`). | Reusable but computationally heavy; currently uses default paths. | **Refactor into a reusable component**. Parameterise file paths; separate simulation functions; integrate as optional advanced analysis panel. |
+| `run_dashboard.py` | Thin launcher script that sets up `sys.path` and calls `networkmodel.dashboard_app.main()`. | None (it only prints “Dashboard launched successfully”). | Not needed in the unified dashboard. | **Deprecate** after integration; retain for backward compatibility. |
+
+## 4. `scripts/` Audit
+
+Every script under `scripts/` was reviewed. The following table summarises their purpose, CLI usage, inputs, outputs and integration recommendation. Scripts without command‑line flags rely on hard‑coded defaults; those should be refactored to accept explicit arguments so the frontend can supply user‑selected files.
+
+| Script | Purpose | CLI usage / parameters | Inputs | Outputs | Duplicates package functionality? | Frontend action | Recommendation |
+|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
+| `analyze_tf_kin_counts.py` | Compute PSite statistics for TFs and kinases; counts how many unique PSites each entity has and how many regulators target each gene[\[4\]](chrome://newtab/). | No CLI flags; call `python scripts/analyze_tf_kin_counts.py` or import `main()` with custom paths[\[4\]](chrome://newtab/). | Reads `data/tfopt_results.xlsx` and `data/kinopt_results.xlsx` by default; expects sheets named “Alpha Values” and “Beta Values”. | CSVs (`tf_beta_psite_counts.csv`, `kin_beta_psite_counts.csv`, `per_gene_num_tfs_num_kinases.csv`) in `results_scripts/` and console summary. | Partially duplicated by the TF/Kinopt dashboards (PSite counts and per‑gene statistics). | Yes – could be triggered as a downloadable summary. | Parameterise inputs/outputs; move computation into backend; call from frontend when needed. |
+| `compare_mechanisms.py` | Streamlit app comparing network mechanisms (see Section 3). | Run via `streamlit run scripts/compare_mechanisms.py`[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Uses `config.toml` defaults to locate networks and data; reads optimisation results. | Interactive viewer only; no file outputs. | Overlaps with the networkmodel dashboard; adds mechanism comparison and network simulation. | Yes – as an optional advanced panel. | Refactor for parameterised inputs; separate heavy simulation; integrate into advanced analysis tab. |
+| `curve_similarity.py` | Compute discrete Fréchet distance between observed and estimated curves per row. Useful for ranking fits[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CLI flags `--tfopt-xlsx`, `--kinopt-xlsx`, `--out-dir`[\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Excel files with “Observed” and “Estimated” sheets; optionally both TF and kinase results. | CSV with Fréchet scores per row. | Unique functionality not available elsewhere. | Yes – implement as on‑demand analysis. | Keep; move computation into backend module; provide asynchronous execution. |
+| `export_subnetworks.py` | Export k‑hop subnetworks around shared nodes from kinase and TF networks[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CLI flags `--input2`, `--input4`, `--outdir`, `--hops`[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Input network CSVs (`input2.csv`, `input4.csv`). | Per‑node subnetworks saved as CSV in `//` and a summary index; optional ZIP. | Stand‑alone utility. | Maybe – for network exploration outside optimisation. | Offer as download tool; integrate into advanced network analysis panel. |
+| `find_protein_accumulators.py` | Identify proteins whose predicted fold‑change greatly exceeds RNA fold‑change (ratio \> 100)[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CLI flags `--prot`, `--rna`[\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CSVs `pred_prot_picked.csv` and `pred_rna_picked.csv` from `networkmodel` outputs. | CSV listing accumulator proteins. | Unique analysis; not present in existing dashboards. | Yes – useful in result browser. | Refactor into backend function; call after predictions are available; display table. |
+| `kinopt_network_readout.py` / `tfopt_network_readout.py` | Older scripts (now replaced by the Streamlit dashboards) to recompute signal flow and generate analysis tables. | CLI requiring `--kinopt-xlsx` / `--tfopt-xlsx`. | Excel results; optional input2/input4 network files. | CSV tables summarising activity, dominance, load and knockout effects. | Duplicated by `app/kinopt.py` and `app/tfopt.py`. | Possibly, but redundant. | Mark as deprecated; rely on dashboard components. |
+| `kinopt_network_viz.py` / `tfopt_network_viz.py` | Old matplotlib‑based visualisation scripts for kinase/TF optimisation results. | CLI requiring Excel results; outputs numerous PNG figures. | Excel results. | Plots saved in output directory. | Duplicated by Streamlit dashboards. | No. | Deprecate; keep only for reproducibility. |
+| `make_kinopt_diagram.py` | Generates a flowchart of the Kinopt algorithm for the manuscript. | No CLI; run manually. | None. | Saves PNG diagram. | Not relevant for frontend. | No. | Exclude from frontend. |
+| `mechanistic_insights.py` | Extract biological insights (refractory period, kinetic lag, transcriptional saturation, feedback gain) using optimised parameters[\[7\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CLI flags for network/data files, kinopt/tfopt parameter JSONs and `--output-dir`[\[7\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Kinase and TF networks (`input2.csv`, `input4.csv`), protein and RNA data (`input1.csv`, `input3.csv`), phospho data (`input1.csv`), JSON parameter files. | CSVs and plots in the specified output directory. | Unique analysis. | Yes – advanced analysis panel. | Refactor to accept parameter dictionaries rather than JSON files; allow users to select metrics; integrate into backend. |
+| `temporal_sensitivity.py` | Perform global sensitivity analysis using SALib and Sobol' indices[\[7\]\[8\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | CLI flags `--results-dir`, `--samples` (must be a power of two)[\[8\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | A completed `networkmodel` result directory; JSON parameter files inside. | CSV of Sobol' first‑order and total‑order indices; Plotly HTML plots of parameter rankings over time[\[8\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md). | Complementary to built‑in networkmodel sensitivity analysis. | Yes – optional analysis after model run. | Offload heavy sampling to worker processes; provide progress feedback; integrate into advanced panel. |
+
+## 5. Result Directory and Output Format Audit
+
+### 5.1 Current conventions
+
+Different modules produce outputs in various layouts, leading to inconsistent parsing. The tests for the global model enforce that optimisation outputs reside in subdirectories named `optimization`, `profiles`, `posterior` and `plots` inside the user‑specified output directory, with CSVs such as `multistart_summary.csv`, `best_fit.csv`, `profile_likelihood_summary.csv` and `posterior_summary.csv`[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md). Predicted timeseries (`pred_prot_picked.csv`, `pred_rna_picked.csv`, `pred_phospho_picked.csv`) live at the top level of the output directory alongside `scalar_objective.csv` (or its alias `pareto_F.csv`). A pickled bundle (`dashboard_bundle.pkl`) stores scalar objectives and metadata[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py).
+
+`kinopt.local` and `tfopt.local` write their Excel results (`*_results.xlsx`), multistart summaries and parameter arrays into a `results` directory defined in `config.toml`. They also copy the final Excel file to `data/ode/` (hard‑coded) and generate plots and an HTML report via `latexit`. The directory structure is flat and does not clearly separate raw tables, plots, logs or metadata.
+
+`protwise/runner` and the network readout scripts produce additional outputs like heatmaps, PCA plots and LaTeX reports. These are saved directly in the `OUT_DIR` without subfolder structure.
+
+### 5.2 Problems
+
+1. **Inconsistent directory layouts** – different optimisers write files into different folder names (`results`, `optimization`, `profiles`, `posterior`, `plots`), making it hard to build a general result browser.
+2. **Missing metadata** – most modules do not write a `metadata.json` capturing the command run, version of the software, time stamps and resolved configuration. Provenance tracking is essential for reproducibility.
+3. **Implicit copying** – `kinopt.local` copies the final Excel results to `data/ode/`, which mixes inputs and outputs.
+4. **Lack of standardised logs** – logs are emitted to the console or separate log directories but are not always saved alongside the results with consistent file names.
+5. **Varied naming conventions** – some outputs use long sheet names (“Alpha Values”), while others use snake_case CSVs; this complicates programmatic discovery.
+
+### 5.3 Proposed result directory contract
+
+To support a robust frontend, a unified result directory structure is proposed:
+
+ results//
+ ├── metadata.json # date/time, command, git commit, resolved config, module version
+ ├── command.txt # exact CLI command executed
+ ├── console.log # captured stdout/stderr
+ ├── config_resolved.yaml # full configuration used for the run
+ ├── tables/ # CSV/TSV/XLSX result tables (e.g., multistart summaries, parameter matrices)
+ ├── plots/ # static image files (PNG/SVG) and interactive HTML/JSON for Plotly
+ ├── logs/ # per‑run log files if separate from console.log
+ ├── reports/ # HTML or PDF reports generated by latextit/markdown
+ └── artifacts/ # pickled bundles, NPZ arrays, NPZ parameter samples, etc.
+
+Each module should accept an explicit `--outdir` parameter, create the directory if it does not exist and write all outputs relative to that folder. Hard‑coded copies to `data/ode/` should be removed. The `metadata.json` should include at least: date/time, package version, commit hash, command arguments, data file hashes and time grid used. This contract mirrors the information needed by the Streamlit dashboards and ensures that the frontend can parse results consistently.
+
+### 5.4 Comparison with existing outputs
+
+| Module / script | Meets proposed contract? | Required changes |
+|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `networkmodel/runner.py` | **Partial.** Writes a pickled bundle, `scalar_objective.csv`, convergence history and prediction CSVs, and organises optimisation, profiles and posterior outputs into subfolders. Lacks `metadata.json` and `command.txt`, and logs are not always captured. | Add metadata and provenance files; save resolved configuration; unify names (e.g., always write `scalar_objective.csv` rather than `pareto_F.csv`); create `tables/` and `plots/` subfolders instead of mixing files in top‑level output. |
+| `kinopt.local` / `tfopt.local` | **No.** Flat output in `results`; no metadata; copying to `data/ode`; no standardised logs. | Accept `--outdir`; create subfolders; write metadata; record CLI command; avoid copying results to data directory; separate tables from plots. |
+| `protwise/runner` | **Partial.** Writes predictions and reports but without metadata. | Same as above: unify output structure; include metadata and provenance files. |
+| Post‑processing scripts | **Varies.** Most scripts accept an `--out-dir` and write CSVs/plots inside, but they do not write metadata or logs. | Standardise by writing into `tables/` and `plots/` subfolders and including a brief metadata file (script name, parameters). |
+
+## 6. Module‑Level Frontend Readiness
+
+### 6.1 Kinopt
+
+- **CLI availability:** Two flavours (`local` and `evol`) with `argparse` CLIs. Typer wrapper in `config/cli.py` simplifies invocation.
+- **Input validation:** Arguments are parsed but input files are implicitly read from paths defined in `config.toml`. Missing file errors propagate as Python exceptions. No interactive validation.
+- **Output consistency:** Outputs are placed in a flat `results` directory; Excel file names are fixed; some results copied to `data/ode/`.
+- **Plotting/export behaviour:** A large number of static plots are generated using matplotlib and saved into the same directory. A LaTeX report is produced via `latexit`.
+- **Missing‑data handling:** Options such as `--estimate_missing_kinases` allow imputation, but the CLI does not check whether required columns are present in input CSVs.
+- **Error reporting:** Errors propagate through Python traces; no structured error messages for user consumption.
+- **Frontend integration risks:** Long‑running multistart optimisation may block the UI; the code writes to disk and expects certain directory names; logs and progress are not streamed.
+- **Required refactors:** Factor out core optimisation into a function that accepts inputs and returns results; add progress callbacks; standardise output directory; ensure the function raises informative exceptions; return results as data structures for immediate plotting.
+
+### 6.2 TFopt
+
+- **CLI availability:** Similar to Kinopt; separate `local` and `evol` modules; Typer wrapper.
+- **Input validation:** Data loading functions assume particular sheet names and column order; poor handling of missing or malformed input.
+- **Output consistency:** Same issues as Kinopt: flat directory; Excel results; plots without metadata.
+- **Plotting/export behaviour:** Many static plots; report via `latexit`.
+- **Integration risks:** Large matrices and multistart optimisation may take time; network visualisation (gravis) requires Graphviz; potential memory use.
+- **Required refactors:** Provide function‐level API; standardise outputs; allow asynchronous execution; better input validation and user feedback; ensure dependencies (gravis, pygraphviz) are optional.
+
+### 6.3 Protwise
+
+- **CLI availability:** `protwise.runner.main` is invoked via pixi task or Typer wrapper; accepts configuration file to run protein dynamics simulations.
+- **Input validation:** Expects configuration with parameter bounds and initial conditions; may fail with cryptic error if not provided.
+- **Output consistency:** Writes predictions and plots in its output directory but lacks metadata and standard subfolder structure.
+- **Integration risks:** JAX/NumPy heavy ODE solver; may require GPU or CPU thread control; long runtime; numerous plots and sensitivity analyses.
+- **Required refactors:** Unify with global result contract; export predictions as CSV; provide progress updates; catch exceptions.
+
+### 6.4 Networkmodel
+
+- **CLI availability:** `networkmodel/runner.py` uses `argparse` and accepts many parameters (paths, regularisation values, solver choice, iteration counts)[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md).
+- **Input validation:** Some checks for file existence and consistent shapes; still heavy reliance on defaults from `config.toml` and implicit assumptions (e.g., presence of kinopt/tfopt results for parameter initialisation).
+- **Output consistency:** Better than other modules; results placed into subfolders (`optimization`, `profiles`, `posterior`, `plots`) and pickled bundle saved[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py); however metadata and command provenance are missing.
+- **Integration risks:** Long runtimes (hyperparameter scanning, sensitivity analysis, posterior sampling); heavy memory usage; JAX/NumPy error messages; concurrency issues if run inside Streamlit process.
+- **Required refactors:** Provide API to run the model asynchronously (e.g., via multiprocessing); unify result directory; capture metadata; export predictions and objective values in separate tables; decouple dashboard bundling from run to allow user‑selected result directories; ensure logs and errors are captured for UI.
+
+## 7. Proposed Unified Frontend Architecture
+
+The unified no‑code frontend should wrap existing scientific modules without re‑implementing their logic. It should consist of three conceptual layers:
+
+ Frontend (Streamlit UI)
+ ├─ Wrapper (command builder + launcher)
+ ├─ Result viewer (table/plot/log/metadata browser)
+ └─ Workflow panels (for kinopt, tfopt, protwise, networkmodel)
+
+ CLI modules (source of truth) ↔ Scientific modules (model logic)
+
+### 7.1 Core components
+
+1. **Upload panel** – allows users to upload raw data (CSV or Excel) or preprocessed results. Performs basic validation and previews files. Supports selecting sample configuration files.
+
+2. **Workflow selector** – lists available workflows (`prep`, `tfopt`, `kinopt`, `model`, `networkmodel`) and shows a description and estimated runtime.
+
+3. **Configuration panel** – exposes CLI arguments for the selected workflow. For example, for `kinopt` and `tfopt`, the user can choose bounds, loss type, scaling method, segmentation points and number of multistart runs. For `networkmodel`, users can set regularisation λ values, solver choice, generation count and toggle sensitivity or hyperparameter scanning. Each option should display the default value and help text.
+
+4. **Command preview** – shows the exact command that will be executed (e.g., `python -m kinopt.local --lower_bound -4 --upper_bound 4 --loss_type weighted ...`). This transparency builds trust and allows advanced users to copy the command for manual runs. The preview updates when the user modifies options.
+
+5. **Pixi environment selector** – allows selecting a Pixi environment (`default`, `dev`, `viz`, `full`) before launching the command. It may call `pixi run` under the hood.
+
+6. **Execution console** – a console panel streams stdout/stderr and displays progress. Long‑running jobs should run in separate worker processes to keep the UI responsive. Provide cancellation and show runtime and status at completion. Save the command and console output to `command.txt` and `console.log` in the result directory.
+
+7. **Result browser** – after a run, detect the `results/` directory and display a tree of available outputs. Users can navigate into tables, plots, logs, reports and artifacts. Provide viewers for DataFrames (sorting/filtering), images (with download), interactive Plotly charts, log files, HTML/PDF reports and metadata. Offer a ZIP download of the entire run.
+
+8. **Workflow‑specific panels** – for each major module:
+
+9. **Kinopt panel** – embed computations and visualisations from `app/kinopt.py`; allow uploading results or linking from a recent run; provide options to recompute knockout effects and network filters.
+
+10. **TFopt panel** – embed `app/tfopt.py` similarly.
+
+11. **Protwise panel** – design a new panel summarising ODE predictions, parameter profiles, PCA/t‑SNE results, residual plots and sensitivity indices.
+
+12. **Networkmodel panel** – reuse `networkmodel/dashboard_app.py` to view global optimisation results; add additional tabs for inference outputs and advanced analyses.
+
+13. **Advanced analysis panel** – integrate functions for Fréchet distance, accumulator detection, mechanistic insights and temporal sensitivity. Provide parameter sliders and run analyses asynchronously.
+
+### 7.2 Interaction flow
+
+1. **Load data** – user uploads or selects raw files; `prep` can be run if necessary.
+2. **Optimise TF or kinase modules** – user configures parameters and launches `tfopt` or `kinopt`. On completion, immediate analysis is available via their respective panels.
+3. **Run Protwise model** – using optimised alpha/beta values, the user runs the ODE model and explores results in the Protwise panel.
+4. **Run global model** – user configures and launches `networkmodel` optimisation; results are explored via the Networkmodel panel and advanced analyses.
+5. **Analyse results** – through the result browser and analysis panels, the user inspects outputs, compares fits, performs sensitivity analyses and exports findings.
+
+### 7.3 Separation of concerns
+
+To maintain maintainability and reproducibility, the frontend must **not** implement or duplicate scientific computations. All optimisation and modelling should be executed via existing CLI modules. The frontend acts as an orchestrator, passing validated arguments, launching subprocesses and visualising outputs. When new features are needed, the scientific code should be extended (e.g., adding an API function) rather than being re‑written in the UI layer.
+
+## 8. Integration Plan for Existing Apps
+
+Integration should proceed module by module:
+
+1. **Networkmodel dashboard** – wrap `networkmodel/dashboard_app.py` as a component that accepts a result directory and renders the existing tabs. Adjust to read the unified result contract and display metadata.
+2. **Kinopt and TFopt dashboards** – refactor `app/kinopt.py` and `app/tfopt.py` into modules exposing functions that accept dataframes and parameters and return Streamlit layouts. The unified dashboard can call these functions after a run or when a user uploads results. Remove duplicated logic for reading from Excel; rely on result tables.
+3. **Compare mechanisms** – break `scripts/compare_mechanisms.py` into backend functions for loading data, running steady‑state simulations and producing graphs. Provide a thin UI wrapper for integration into the advanced analysis panel. Offer asynchronous execution and caching.
+4. **Legacy visualisation scripts** – mark `kinopt_network_viz.py`, `tfopt_network_viz.py` and network readout scripts as deprecated. Their functionality is replaced by interactive dashboards. Keep them only for batch use or reproducibility.
+5. **Post‑processing utilities** – incorporate computations from scripts (Fréchet distance, accumulator detection, mechanistic insights, temporal sensitivity) into backend functions with clean APIs. Provide UI triggers for these analyses with progress feedback. Offer results in the result browser and advanced panel.
+
+## 9. Frontend Build Phases
+
+### Phase 0 — Audit and stabilisation
+
+- Complete this repository audit and agree on the standard result directory contract and metadata schema.
+- Refactor modules to accept explicit `--outdir`, write metadata and provenance files, and avoid copying outputs into input folders.
+- Remove hard‑coded default paths in scripts; parameterise inputs and outputs.
+
+### Phase 1 — Result browser integration
+
+- Implement a base Streamlit app with a sidebar for selecting a results directory.
+- Build functions to parse the new result directory structure: list tables, plots, logs, reports and artifacts; read metadata; and return DataFrames or binary content.
+- Add viewers for tables (using pandas and Streamlit), images, interactive Plotly charts and logs. Provide download buttons.
+- Support zipped download of an entire result run.
+
+### Phase 2 — CLI launcher
+
+- Implement a command builder UI that lists available workflows (`prep`, `tfopt`, `kinopt`, `model`, `networkmodel`) and exposes their arguments. Use CLI descriptions for help texts.
+- Show a command preview that updates with user selections. Provide a “Run” button that launches the command in a separate process or Pixi environment. Stream stdout/stderr to the UI and save them.
+- Provide cancellation and error handling. On completion, detect the new results directory and open it in the result browser.
+
+### Phase 3 — Upload and configuration UI
+
+- Implement file upload for raw datasets and existing results. Validate file formats and show previews.
+- Add configuration panels for each workflow with defaults from `config.toml`. Allow saving/loading presets.
+- Allow selection of Pixi environment and detect missing optional dependencies.
+
+### Phase 4 — Workflow‑specific panels
+
+- **Kinopt panel:** integrate computation and visualisation from `app/kinopt.py`; allow uploading results or linking from a previous run; provide options to recompute knockout effects and network filters.
+- **TFopt panel:** integrate `app/tfopt.py` similarly.
+- **Protwise panel:** design a new panel summarising ODE predictions, parameter profiles, PCA/t‑SNE results, residual plots and sensitivity indices.
+- **Networkmodel panel:** reuse `networkmodel/dashboard_app.py`; add additional tabs for inference outputs and advanced analyses.
+- **Advanced analysis panel:** integrate functions for Fréchet distance, accumulator detection, mechanistic insights and temporal sensitivity. Provide parameter sliders and run analyses asynchronously.
+
+### Phase 5 — Testing and documentation
+
+- Write unit tests for the command builder, result parser and workflow registry. Mock CLI commands to ensure commands are constructed correctly.
+- Add tests for file discovery and parsing across various result directory layouts.
+- Write integration tests for script functions that compute Fréchet distance, accumulators and sensitivity indices.
+- Document usage of the dashboard, including how to upload data, configure workflows, run optimisations and browse results. Update MkDocs accordingly.
+
+## 10. Required Code Changes Before Frontend Implementation
+
+1. **Consistent** `--outdir` **support** – all CLI modules must accept an explicit `--outdir` parameter and write all outputs relative to it. Default values can come from `config.toml`, but overrides must be honoured.
+2. **Metadata and provenance** – every run should generate `metadata.json`, `command.txt` and `console.log` capturing the command, parameters, software version, commit and timestamps.
+3. **Predictable subfolders** – adopt the proposed `tables/`, `plots/`, `logs/`, `reports/` and `artifacts/` layout. Group plots by category.
+4. **Consistent naming** – unify file names (use snake_case; avoid spaces); always write `scalar_objective.csv` rather than alias `pareto_F.csv`.
+5. **Centralised logging** – log to both console and a file in the run directory; support verbosity flags.
+6. **Error handling** – catch common errors (missing files, malformed data) and emit user‑friendly messages; return non‑zero exit codes.
+7. **API functions** – expose core functionality as importable functions so the frontend can call them directly; CLI wrappers should delegate to these functions.
+8. **Remove implicit copies** – avoid copying results into `data/ode/` or other input directories; keep outputs within the result directory.
+9. **No absolute paths** – ensure file paths in results and metadata are relative; sanitise paths in logs.
+10. **Refactor scripts** – modify scripts to accept explicit input/output parameters, write metadata, and return data structures; provide a module interface (e.g., `def run_curve_similarity(...)`).
+
+## 11. Proposed File Structure
+
+To organise the unified frontend and keep it separate from the core package, add a `dashboard/` directory at the repository root. The following file structure is proposed (names may be adjusted):
+
+ dashboard/
+ ├── app.py # Streamlit entry point; sets up pages and routing
+ ├── command_builder.py # Builds CLI commands from user selections
+ ├── runner.py # Launches subprocesses or calls backend API asynchronously
+ ├── result_parser.py # Discovers result directories and parses tables/plots/logs/metadata
+ ├── file_utils.py # Helper functions for file upload, validation and zip download
+ ├── registry.py # Registers available workflows, their arguments and execution functions
+ ├── components/
+ │ ├── upload_panel.py # Handles file uploads and previews
+ │ ├── workflow_selector.py
+ │ ├── command_preview.py
+ │ ├── console_panel.py
+ │ ├── result_browser.py
+ │ ├── table_viewer.py
+ │ ├── plot_viewer.py
+ │ ├── log_viewer.py
+ │ └── download_panel.py
+ └── workflow_panels/
+ ├── kinopt.py # Wraps the Kinopt dashboard logic
+ ├── tfopt.py # Wraps the TFopt dashboard logic
+ ├── protwise.py # Summarises Protwise outputs
+ ├── networkmodel.py # Embeds the global dashboard
+ └── analysis.py # Advanced analysis panels (Fréchet, accumulators, sensitivity)
+
+ tests/dashboard/
+ ├── test_command_builder.py
+ ├── test_result_parser.py
+ ├── test_registry.py
+ └── test_file_utils.py
+
+This structure isolates UI components, workflow definitions and utility functions. Each `workflow_panels/*.py` file should import the corresponding backend module, call its API functions and use Streamlit to present outputs. The tests ensure that commands are built correctly, results are parsed and that the registry contains all expected workflows.
+
+## 12. Pixi and Dependency Plan
+
+The current `pixi.toml` lists numerous dependencies, some of which are heavy or optional. For the frontend, only a subset is required. Recommended dependencies:
+
+| Package | Purpose | Optional? |
+|---------------------------------------|---------------------------------------------------------------------------------------|--------------------------------------------------------------|
+| **streamlit** | Web UI framework; core requirement | No |
+| **pandas** | DataFrame operations and CSV/XLSX parsing | No |
+| **openpyxl** | Read Excel files in upload panels and TF/Kinopt dashboards | Yes (only if Excel preview needed) |
+| **plotly** | Interactive plots in dashboards | Yes (can fallback to matplotlib) |
+| **pyarrow** | For Parquet support in large tables | Yes |
+| **gravis**, **networkx**, **imageio** | Required for network visualisation in TF/Kinopt dashboards and compare_mechanisms app | Optional; should be loaded only when network panels are used |
+
+Pixi tasks should be added for the dashboard. In `pixi.toml`, under `[tasks]`, add:
+
+ dashboard = "streamlit run dashboard/app.py"
+ dashboard-dev = "streamlit run dashboard/app.py --server.runOnSave true"
+
+Users can then run `pixi run dashboard` or `dashboard-dev`. The frontend should detect missing optional dependencies and disable features accordingly.
+
+## 13. Risks and Mitigations
+
+| Risk | Description | Mitigation |
+|---------------------------------------------------------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Duplicate dashboards** | Existing Streamlit apps may diverge from the unified frontend. | Keep original apps for backward compatibility, but route users to the unified dashboard. Import existing dashboard functions instead of re‑implementing logic. |
+| **Inconsistent output folders** | Modules currently write results into varying layouts. | Refactor modules to adopt the proposed result contract. Implement the result browser to handle legacy layouts gracefully (e.g., recognise `optimization`, `profiles`, `plots` subfolders). |
+| **Scripts bypass package logic** | Scripts reimplement analysis instead of importing functions. | Refactor scripts into backend modules; use them in both CLI and UI; deprecate direct script usage. |
+| **Frontend becomes a second scientific implementation** | Risk of duplicating computations in the UI. | Keep scientific code exclusively in backend modules; the UI calls them via API functions or CLI wrappers. |
+| **Broken CLI commands** | Parameter changes or refactors may break existing pipelines. | Write regression tests for each CLI entry point; maintain backward compatibility through Typer wrappers; document deprecated flags. |
+| **Large result folders** | Output directories may contain hundreds of MB of plots, NPZ files and videos. | Implement lazy loading of plots; compress images for web; allow users to choose which subfolders to load; offer zipped downloads. |
+| **Long‑running jobs blocking Streamlit** | Optimisation and inference can take minutes or hours. | Run jobs in separate processes or using asynchronous workers; show progress bars and allow cancellation; persist results to disk for later review. |
+| **Missing provenance** | Without metadata, users cannot reproduce runs. | Enforce writing of `metadata.json`, `command.txt` and `console.log` on every run; display metadata in the result browser. |
+| **Hidden local paths** | Absolute paths in outputs may expose local directory structure. | Use relative paths in outputs; sanitise paths in metadata and logs. |
+
+## 14. Final Recommendation
+
+The PhosKinTime repository contains a rich suite of optimisation algorithms, ODE models, analysis scripts and interactive dashboards. Before building a no‑code frontend, stabilise the backend by refactoring modules to accept explicit output directories, write metadata and adopt a unified result layout. De‑duplicate analysis logic by moving computations into importable functions and reusing them in both CLI scripts and Streamlit dashboards. Once the backend is standardised, proceed with the phased implementation of the unified dashboard: start with a result browser, then add a command builder and upload/configuration panels, followed by workflow‑specific panels that embed existing dashboards. Finally, implement advanced analysis panels and comprehensive tests. By following this plan, the no‑code frontend will provide a seamless experience for researchers while preserving the integrity and reproducibility of the underlying scientific computations.
+
+[\[1\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) [\[5\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) [\[6\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) [\[7\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) [\[8\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/scripts.md) \[title unknown\]
+
+
+
+[\[2\]](file:///home/oai/share/phoskintime/phoskintime-global/config/cli.py) \[title unknown\]
+
+
+
+[\[3\]](file:///home/oai/share/phoskintime/phoskintime-global/docs/Documentation/dashboard.md) \[title unknown\]
+
+
+
+[\[4\]](chrome://newtab/) \[title unknown\]
+
+
+
+### Phase 0 implementation note (June 2026)
+
+The backend output contract is implemented with shared result utilities in `common/results.py`. The local kinase, local TF, ProtWise runner, network model runner, and Typer workflow wrappers now accept `--outdir`/`--output-dir` and initialize the standard run subdirectories (`tables/`, `plots/`, `logs/`, `reports/`, `artifacts/`) plus provenance files (`metadata.json`, `command.txt`, `console.log`, and `config_resolved.yaml` when a resolved configuration is available). Legacy top-level output filenames are retained for compatibility, and dashboard-facing copies are mirrored into the standard subfolders.
+
+### Phase 1 implementation note (June 2026)
+
+The initial dashboard component lives under `dashboard/` and focuses only on browsing existing result directories. It discovers the Phase 0 output contract and selected legacy outputs, lazily loads tables/plots/logs/reports only after a user selects them, and provides an in-memory ZIP archive for downloading a result directory. Workflow launching and file uploads remain outside this phase.
+
+### Phase 2 implementation note (June 2026)
+
+The dashboard now includes a registered workflow launcher that builds argv-list commands for existing Pixi/Python entry points, previews commands before execution, runs subprocesses from the repository root with `shell=False`, streams merged stdout/stderr into the UI and `console.log`, records launcher provenance, and opens successful run directories in the result browser. Upload/config editing remains out of scope. Interactive cancellation is not exposed yet because reliable Streamlit cancellation needs a persistent background job supervisor; the runner has a cancellation callback boundary for a future supervised implementation.
+
+### Phase 3 implementation note (June 2026)
+
+The dashboard now supports no-code setup before launching registered workflows: users can upload supported input/config files into `dashboard_uploads//`, preview CSV/TSV/XLSX data, assign files to workflow-specific input roles, edit structured parameters based on the actual CLI flags, validate basic file problems, and save or download JSON/YAML presets. These selections are converted into the existing argv-list command builder so execution still goes through the original CLI modules rather than dashboard-side scientific logic.
+
+### Phase 4 implementation note (June 2026)
+
+Workflow-specific dashboard panels now live under `dashboard/workflow_panels/` for KinOpt, TFOpt, ProtWise, Networkmodel, and advanced analyses. The panels discover and display existing workbooks, CSVs, plots, reports, bundles, and inference outputs from the selected result directory while leaving model fitting, ODE solving, optimization, and expensive analyses in the existing backend scripts/CLI modules. Advanced analyses are exposed as parameterized command builders and are not executed automatically on page load.
+
+
+## 15. Implementation Status
+
+The no-code dashboard phases are implemented in the unified `dashboard/` package. The current implementation provides the standard result contract and provenance helpers, a lazy result browser, a structured command launcher, upload/configuration panels, workflow-specific result panels, advanced-analysis command wrappers, dashboard-focused tests, Pixi dashboard tasks, git-ignore coverage for generated artifacts, and user documentation. Streamlit UI imports are kept inside render functions so importing dashboard modules does not start a Streamlit server or require optional UI dependencies until those render paths are used.
diff --git a/docs/dashboard_developer_guide.md b/docs/dashboard_developer_guide.md
new file mode 100644
index 0000000..b005f12
--- /dev/null
+++ b/docs/dashboard_developer_guide.md
@@ -0,0 +1,183 @@
+# Dashboard developer guide
+
+## Architecture
+
+The dashboard package is organized as a thin UI and orchestration layer around existing backend CLIs.
+
+```text
+dashboard/
+├── app.py
+├── command_builder.py
+├── runner.py
+├── result_parser.py
+├── file_utils.py
+├── registry.py
+├── components/
+└── workflow_panels/
+```
+
+Key responsibilities:
+
+- `app.py` wires the Streamlit tabs together.
+- `registry.py` describes workflows, CLI flags, input assignment roles, and file-extension rules.
+- `command_builder.py` converts structured selections into argv lists.
+- `runner.py` executes subprocesses from the repository root and streams logs.
+- `result_parser.py` lazily inventories standard and legacy result layouts.
+- `file_utils.py` handles safe path, upload, preview, and ZIP helpers.
+- `components/` contains reusable Streamlit render functions.
+- `workflow_panels/` contains workflow-specific result viewers and analysis command wrappers.
+
+## Design principles
+
+- Do not put scientific computation in Streamlit UI files.
+- Do not execute user-generated shell strings.
+- Never use `shell=True` for dashboard-built workflow commands.
+- Build commands as `list[str]` argv values.
+- Backend CLI remains the source of truth.
+- Result parsing must support both the standard result directory contract and practical legacy layouts.
+- Optional dependencies must fail gracefully and only be imported on the render path that needs them.
+- Importing dashboard modules should not start Streamlit.
+- File validation must match backend readers.
+
+## Workflow registry
+
+`dashboard/registry.py` defines `WorkflowDescriptor`, `ArgumentSpec`, and `InputSpec` records.
+
+Registry rules:
+
+- Accepted file extensions must match backend readers.
+- Required inputs must match CLI behavior.
+- Labels/help text must not overpromise support.
+- Config `InputSpec` extensions passed as `--conf` must remain `.toml` unless the target runner implements YAML/JSON parsing with tests.
+- `--conf` must be forwarded and honored by the backend runner.
+- Explicit CLI flags override config values.
+- `output_dir_arg` must match the workflow CLI (`--outdir` or `--output-dir`).
+- `safe_for_dashboard` should be `False` for result-only descriptors or unsafe workflows.
+
+Important file-format examples:
+
+- ProtWise protein input is CSV because the runner reads it with `pd.read_csv`.
+- Networkmodel network/data inputs are CSV because `networkmodel.io.load_data` uses default `pd.read_csv`.
+- Previous KinOpt/TFOpt prior result inputs are Excel workbooks where the backend uses Excel readers.
+
+## Command builder
+
+`dashboard/command_builder.py` resolves a workflow key, structured arguments, input assignments, Pixi environment, and run name into a `BuiltCommand`.
+
+Precedence for configuration and arguments should remain:
+
+```text
+explicit dashboard/CLI selection
+> uploaded/selected config
+> repository default config
+> hard-coded fallback only where unavoidable
+```
+
+Guidelines:
+
+- Reject unknown argument names.
+- Reject unknown input roles.
+- Sanitize run names before constructing output paths.
+- Keep output directories under the project `results/` area unless the user explicitly selects another path.
+- Keep the command preview reproducible.
+
+## Runner
+
+`dashboard/runner.py` runs command lists with `subprocess.Popen(..., shell=False)` from the repository root. It merges stdout/stderr, streams output to the dashboard, and writes `console.log` in the run directory.
+
+Return status conventions:
+
+- `success` when return code is `0`.
+- `failure` when return code is non-zero.
+- `cancelled` when cancellation is requested and the process is terminated.
+
+Interactive cancellation is intentionally limited until a robust background job supervisor is introduced. The runner exposes a cancellation callback boundary for supervised execution.
+
+## Result parser
+
+`dashboard/result_parser.py` inventories files without eagerly loading large content. The parser recognizes:
+
+- standard files: `metadata.json`, `command.txt`, `console.log`, `config_resolved.yaml`;
+- standard folders: `tables/`, `plots/`, `logs/`, `reports/`, `artifacts/`;
+- legacy Networkmodel outputs such as `scalar_objective.csv`, prediction CSVs, `optimization/`, `profiles/`, `posterior/`, and `plots/`;
+- legacy local result workbooks such as `kinopt_results.xlsx` and `tfopt_results.xlsx`.
+
+Load file contents only inside viewer components after the user selects a file.
+
+## Workflow panels
+
+Workflow panels should present existing outputs and call reusable backend functions where needed. Do not copy scientific logic into Streamlit callbacks.
+
+Existing apps and scripts to consider when adding or improving panels:
+
+- `app/kinopt.py`
+- `app/tfopt.py`
+- `networkmodel/dashboard_app.py`
+- `scripts/compare_mechanisms.py`
+
+Refactor shared logic into reusable backend functions rather than copying code into dashboard files.
+
+## Adding a new workflow
+
+```text
+1. Add or verify backend CLI
+2. Ensure --outdir support
+3. Ensure metadata/command/log output
+4. Add registry entry
+5. Add input specs
+6. Add command builder tests
+7. Add result parser tests
+8. Add docs
+```
+
+Additional checklist:
+
+- Verify `--conf` precedence if the workflow supports config files.
+- Add result-panel discovery helpers for workflow-specific outputs.
+- Add missing-file tests.
+- Add file-format validation tests matching backend readers.
+- Ensure generated outputs are ignored by Git.
+
+## Testing
+
+Run the full configured dev test task:
+
+```bash
+pixi run -e dev test
+```
+
+Useful focused tests:
+
+```text
+tests/dashboard/test_command_builder.py
+tests/dashboard/test_result_parser.py
+tests/dashboard/test_registry.py
+tests/dashboard/test_file_utils.py
+tests/dashboard/test_runner.py
+tests/dashboard/test_upload_validation.py
+tests/dashboard/test_workflow_panels.py
+tests/test_result_contract.py
+tests/test_networkmodel_runner_config.py
+tests/test_protwise_config_handling.py
+```
+
+Docs build:
+
+```bash
+pixi run -e docs docs-build
+```
+
+## Git hygiene
+
+Generated/local folders should remain ignored and uncommitted:
+
+```text
+dashboard_uploads/
+results/
+logs/
+*.zip
+.streamlit/
+__pycache__/
+```
+
+Do not commit result folders, uploaded files, logs, ZIP archives, local Streamlit state, or cache directories.
diff --git a/docs/dashboard_quickstart.md b/docs/dashboard_quickstart.md
new file mode 100644
index 0000000..84c603d
--- /dev/null
+++ b/docs/dashboard_quickstart.md
@@ -0,0 +1,111 @@
+# Dashboard quickstart
+
+The PhosKinTime no-code dashboard is a Streamlit interface for users who want to run existing PhosKinTime workflows and inspect completed result folders without writing Python code.
+
+## What the dashboard does
+
+- Browses existing PhosKinTime result directories.
+- Shows metadata, commands, logs, tables, plots, reports, artifacts, and ZIP downloads.
+- Lets users upload/select input files into a dashboard-managed upload folder.
+- Builds and previews safe CLI/Pixi commands for registered workflows.
+- Runs registered workflows as subprocesses and streams console output.
+
+## What the dashboard does not do
+
+- It does not replace the CLI or Python API.
+- It does not implement new scientific models.
+- It does not silently convert file formats.
+- It does not copy uploaded files into `data/` unless a user or backend command explicitly does so.
+- It does not make long-running optimization jobs instant.
+
+## Install dependencies
+
+From the repository root:
+
+```bash
+pixi install
+```
+
+For development and test dependencies:
+
+```bash
+pixi run -e dev test
+```
+
+## Launch the dashboard
+
+```bash
+pixi run dashboard
+```
+
+Development mode with Streamlit reload-on-save:
+
+```bash
+pixi run dashboard-dev
+```
+
+After Streamlit starts, the terminal prints a local URL, usually `http://localhost:8501`. Open that URL in a browser. If Streamlit can open a browser automatically on your machine, it may do so.
+
+## Browse existing results
+
+1. Open the **Browse results** tab.
+2. Select a base folder, commonly `results`.
+3. Pick a result directory.
+4. Inspect metadata, command, logs, tables, plots, reports, artifacts, and workflow-specific panels.
+5. Use the **Download** tab to create a ZIP archive in memory.
+
+## Run a workflow from the dashboard
+
+1. Open the **Run workflow** tab.
+2. Select a workflow.
+3. Upload or select files.
+4. Assign files to workflow input roles.
+5. Review validation messages.
+6. Preview the generated command.
+7. Click **Run workflow**.
+8. Watch console output and open the completed result directory.
+
+## Upload input files
+
+Uploaded files are saved under:
+
+```text
+dashboard_uploads//
+```
+
+Supported upload extensions are `.csv`, `.tsv`, `.xlsx`, `.yaml`, `.yml`, `.json`, and `.txt`. Workflow execution validation may be stricter than upload validation. Config files passed as `--conf` currently must be `.toml`, and Networkmodel execution inputs plus the ProtWise protein input currently use CSV readers in the backend, so those assigned data inputs must be `.csv`.
+
+## Download results
+
+The result browser can package the selected result directory into a ZIP file. The ZIP is generated in memory for browser download and should not be committed to Git.
+
+## Minimal command examples
+
+```bash
+pixi install
+pixi run dashboard
+pixi run dashboard-dev
+pixi run -e dev test
+```
+
+Workflow examples:
+
+```bash
+pixi run kinopt-local --outdir results/example_kinopt
+pixi run tfopt-local --outdir results/example_tfopt
+pixi run model --conf config.toml --outdir results/example_protwise
+pixi run networkmodel --conf config.toml --output-dir results/example_networkmodel
+```
+
+## Checklist
+
+```text
+1. Start dashboard
+2. Select or upload files
+3. Select workflow
+4. Preview command
+5. Run
+6. Inspect console
+7. Browse results
+8. Download ZIP
+```
diff --git a/docs/dashboard_troubleshooting.md b/docs/dashboard_troubleshooting.md
new file mode 100644
index 0000000..538e63d
--- /dev/null
+++ b/docs/dashboard_troubleshooting.md
@@ -0,0 +1,216 @@
+# Dashboard troubleshooting
+
+## Quick diagnostic table
+
+| Symptom | Likely cause | Fix | Diagnostic command |
+| --- | --- | --- | --- |
+| Dashboard does not start | Dependencies missing or Pixi environment incomplete | Reinstall and run dashboard from repo root | `pixi install && pixi run dashboard` |
+| Streamlit import fails | Streamlit missing from active env | Use Pixi environment, not system Python | `pixi run python -c "import streamlit; print(streamlit.__version__)"` |
+| Pixi command fails | Pixi not installed or environment broken | Reinstall Pixi/env | `pixi run python --version` |
+| Uploaded file rejected | Registry extension validation failed | Use the backend-supported extension | Check dashboard validation panel |
+| Custom `--conf` appears ignored | Runner imported config constants too early or CLI field overrides config | Inspect provenance | `cat results//metadata.json` |
+| Result folder is empty | Workflow failed, wrong `--outdir`, permissions, or wrong selected folder | Inspect logs and command | `cat results//console.log` |
+| Tables/plots are missing | Legacy output layout or workflow wrote elsewhere | Check metadata/command output path | `cat results//command.txt` |
+| UI freezes during long run | Heavy workflow running in Streamlit callback/process | Use subprocess workflow launcher; wait for logs | Inspect `console.log` |
+| Optional visualization missing | Optional package not installed | Add dependency with Pixi | `pixi add plotly` |
+| Tests fail after dashboard edits | Registry/command/result contract changed | Run focused tests | `pixi run -e dev pytest tests/dashboard -v` |
+
+## Dashboard does not start
+
+Install or refresh dependencies:
+
+```bash
+pixi install
+pixi run dashboard
+```
+
+Check Streamlit:
+
+```bash
+pixi run python -c "import streamlit; print(streamlit.__version__)"
+```
+
+Run from the repository root. Streamlit should print a local URL such as `http://localhost:8501`.
+
+## Pixi environment is broken
+
+Basic checks:
+
+```bash
+pixi install
+pixi run python --version
+pixi run -e dev test
+```
+
+If the environment is corrupt, remove and recreate it:
+
+```bash
+rm -rf .pixi/envs/default
+pixi install
+```
+
+## PyCharm notebook/kernel issues
+
+PyCharm may use the wrong interpreter or Jupyter kernel. Check available kernels and the executable:
+
+```bash
+pixi run jupyter kernelspec list
+pixi run python -c "import sys; print(sys.executable)"
+```
+
+The expected interpreter is inside the Pixi environment:
+
+```text
+.pixi/envs/default/bin/python
+```
+
+Register a kernel for notebooks:
+
+```bash
+pixi run python -m ipykernel install --user --name phoskintime --display-name "Python (phoskintime)"
+```
+
+Select `Python (phoskintime)` in PyCharm/Jupyter.
+
+## `pip._internal.operations.build` error
+
+This usually means the Pixi environment's `pip` is corrupted, or PyCharm is trying to install packages through its own helper instead of using Pixi.
+
+Fix:
+
+```bash
+rm -rf .pixi/envs/default
+pixi install
+pixi add pip ipykernel
+```
+
+Do not rely on the PyCharm package installer for Pixi-managed dependencies. Add dependencies through `pixi.toml`/`pixi add` so the environment remains reproducible.
+
+## Uploaded file rejected
+
+The upload panel accepts a broad set of extensions, but workflow assignment validation follows backend readers.
+
+- Config files assigned to workflow `--conf` fields currently must be TOML; YAML/JSON uploads are for preview/preset contexts unless a runner explicitly supports them.
+- ProtWise protein input currently expects CSV when the backend uses `pd.read_csv`.
+- Networkmodel kinase network, TF network, MS/protein, RNA, and phosphoproteomics inputs currently expect CSV with the default `pd.read_csv` reader.
+- ProtWise phosphosite/RNA and previous KinOpt/TFOpt result inputs are Excel where the backend uses Excel readers.
+
+Use the extension shown in the validation message.
+
+## TSV/XLSX accepted but workflow fails
+
+This should not happen after registry validation fixes. If it happens, the dashboard registry and backend reader are inconsistent.
+
+Developer fix:
+
+1. Check the backend reader (`pd.read_csv`, `pd.read_excel`, custom separator, etc.).
+2. Update `dashboard/registry.py` `InputSpec.extensions` to match the reader.
+3. Add a validation test in `tests/dashboard/test_upload_validation.py`.
+4. Update user docs.
+
+## Custom `--conf` appears ignored
+
+Expected priority:
+
+```text
+explicit CLI/dashboard field
+> custom --conf
+> default config.toml
+```
+
+Diagnostics:
+
+```bash
+cat results//metadata.json
+cat results//config_resolved.yaml
+cat results//command.txt
+```
+
+Check these fields:
+
+- supplied config path;
+- resolved config path;
+- config source/custom flag;
+- effective inputs;
+- effective bounds/lambdas/time grids;
+- output directory.
+
+If a workflow imports config constants at module load time, it may ignore custom `--conf`. This is a bug and should be fixed in the runner by parsing `--conf` before importing config-dependent modules.
+
+## Result folder is empty
+
+Likely causes:
+
+- workflow failed before writing outputs;
+- wrong `--outdir`/`--output-dir`;
+- permission issue;
+- dashboard pointed at the wrong result directory.
+
+Diagnostics:
+
+```bash
+cat results//console.log
+cat results//command.txt
+```
+
+If `console.log` is missing, the runner may have failed before result contract initialization.
+
+## Dashboard cannot find tables/plots
+
+Likely causes:
+
+- legacy output layout;
+- workflow did not follow result directory contract;
+- output was written elsewhere;
+- selected directory is a parent folder rather than the run folder.
+
+Fix:
+
+1. Open `metadata.json` and `command.txt`.
+2. Confirm the command used the expected output directory.
+3. Search the selected run folder for generated files.
+4. If needed, update `dashboard/result_parser.py` to recognize a documented legacy layout.
+
+## Long-running workflow freezes UI
+
+Heavy workflows should run as subprocesses or external workers, not as scientific computation inside Streamlit callbacks. The dashboard launcher uses subprocess execution for registered workflows. If a new panel triggers expensive analysis directly, refactor it into a backend CLI/function and call it through the runner or a supervised job mechanism.
+
+## Missing optional dependency
+
+Examples:
+
+- `gravis`
+- `networkx`
+- `imageio`
+- `openpyxl`
+- `plotly`
+
+Install through Pixi only:
+
+```bash
+pixi add openpyxl
+pixi add plotly
+```
+
+For visualization extras already modeled as a Pixi feature, prefer the appropriate Pixi environment if documented.
+
+## Tests fail after dashboard changes
+
+Run the configured test task:
+
+```bash
+pixi run -e dev test
+```
+
+Isolate dashboard tests:
+
+```bash
+pixi run -e dev pytest tests/dashboard -v
+```
+
+Common causes:
+
+- registry `InputSpec` does not match backend reader;
+- command builder accepts unsupported arguments;
+- result parser no longer recognizes a legacy layout;
+- a dashboard module imports Streamlit or optional heavy dependencies at module import time.
diff --git a/docs/dashboard_user_guide.md b/docs/dashboard_user_guide.md
new file mode 100644
index 0000000..cd9dda3
--- /dev/null
+++ b/docs/dashboard_user_guide.md
@@ -0,0 +1,177 @@
+# Dashboard user guide
+
+## Overview
+
+The PhosKinTime dashboard is a no-code wrapper around existing PhosKinTime command-line workflows. It helps users choose files, preview commands, launch supported workflows, monitor logs, and browse output folders.
+
+```text
+Dashboard = wrapper + launcher + result viewer
+CLI = source of truth
+Scientific modules = model logic
+```
+
+The dashboard should be used for convenience and reproducibility. The CLI and backend modules remain authoritative for scientific behavior.
+
+## Supported workflows
+
+| Workflow | Purpose | Expected inputs | Accepted execution formats | Output location | Runtime class | Dashboard execution | Result-only viewing |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| `prep` | Run preprocessing cleanup via `processing.cleanup`. | Files referenced by repository configuration and preprocessing conventions. | Config/data dependent. | Existing preprocessing outputs; no dedicated dashboard output flag. | Short | Yes | Limited |
+| `kinopt-local` / `kinopt` | Local kinase-to-phosphosite optimization. | Protein/kinase abundance CSV, phosphosite CSV/network data, optional config. | Registered config files passed as `--conf`: `.toml`; workflow data follows backend config. | `results/kinopt-local//` when launched by dashboard; CLI `--outdir` supported. | Medium | Yes | Yes |
+| `kinopt-evol` | Evolutionary KinOpt mode exposed as a Pixi task. | Same family of KinOpt inputs, governed by backend config. | Backend dependent. | Backend-defined outputs. | Medium/long | Not directly registered in dashboard launcher | Result folders can be browsed if contract/legacy layout is present |
+| `tfopt-local` / `tfopt` | Local TF-to-mRNA optimization. | mRNA/RNA CSV, TF network CSV, optional config. | Registered config files passed as `--conf`: `.toml`; workflow data follows backend config. | `results/tfopt-local//` when launched by dashboard; CLI `--outdir` supported. | Medium | Yes | Yes |
+| `tfopt-evol` | Evolutionary TFOpt mode exposed as a Pixi task. | Same family of TFOpt inputs, governed by backend config. | Backend dependent. | Backend-defined outputs. | Medium/long | Not directly registered in dashboard launcher | Result folders can be browsed if contract/legacy layout is present |
+| `protwise-model` / `model` | Protein-wise ODE fitting downstream of KinOpt/TFOpt. | Protein CSV, phosphosite Excel workbook, RNA/TFOpt Excel workbook, optional TOML config. | Protein input: `.csv`; phosphosite and RNA result inputs: `.xlsx`; config passed as `--conf`: `.toml`. | `results/protwise-model//` when launched by dashboard; CLI `--outdir` supported. | Medium/long | Yes | Yes |
+| `networkmodel` | Coupled kinase-signaling and TF/RNA global network model. | Kinase network CSV, TF network CSV, MS/protein CSV, RNA CSV, optional phosphoproteomics CSV, optional KinOpt/TFOpt Excel priors, TOML config. | Network/data inputs read by backend with `pandas.read_csv`: `.csv` only; prior result workbooks: `.xlsx`. | `results/networkmodel//` when launched by dashboard; CLI `--output-dir`/`--outdir` supported. | Long | Yes | Yes |
+| `phoskintime-all` | Typer wrapper for preprocessing, local TFOpt, local KinOpt, and ProtWise. | Stage-specific configs for TFOpt/KinOpt/ProtWise. | Config assignment for CLI execution: `.toml`. | `results/phoskintime-all//` when launched by dashboard. | Long | Yes | Stage result folders can be browsed |
+| Advanced analysis scripts | Existing analysis utilities such as curve similarity, protein accumulator detection, subnetwork export, mechanistic insights, temporal sensitivity, and mechanism comparison. | Existing result folders/tables, depending on script. | Script-specific; do not assume upload conversion. | Selected run directory under `tables/`, `plots/`, `reports/`, or `artifacts/` when integrated. | Medium/long | Exposed as parameterized wrappers where available; not run automatically | Yes |
+
+## Launching the dashboard
+
+Install dependencies first:
+
+```bash
+pixi install
+```
+
+Start the user dashboard:
+
+```bash
+pixi run dashboard
+```
+
+Start development mode with reload-on-save:
+
+```bash
+pixi run dashboard-dev
+```
+
+Streamlit prints a local browser URL such as `http://localhost:8501`.
+
+## Browsing existing results
+
+Use **Browse results** to select a result directory. The dashboard discovers standard contract files and known legacy outputs.
+
+The browser can show:
+
+- `metadata.json` provenance.
+- `command.txt` command provenance.
+- `console.log` and `logs/*`.
+- Tables from `tables/*.csv`, `tables/*.tsv`, and `tables/*.xlsx`.
+- Plots from `plots/*.png`, `plots/*.jpg`, `plots/*.jpeg`, `plots/*.svg`, and `plots/*.html`.
+- Reports from `reports/*.html`, `reports/*.md`, and PDFs where possible.
+- Artifacts from `artifacts/*`.
+- A downloadable ZIP archive of the selected result directory.
+
+If standard files are missing, the dashboard still attempts to show recognized legacy outputs and reports what is missing.
+
+## Uploading files
+
+Uploaded files are stored in:
+
+```text
+dashboard_uploads//
+```
+
+Uploaded files are not silently copied into `data/`. The command preview points to the uploaded/selected path used by the backend.
+
+General upload extensions accepted by the UI for preview or preset storage are:
+
+```text
+.csv, .tsv, .xlsx, .yaml, .yml, .json, .txt
+```
+
+Workflow execution validation is stricter and follows backend readers:
+
+- Config files assigned to workflow `--conf` fields currently must be TOML because the target runners parse TOML.
+- ProtWise protein input currently expects CSV because the backend reads it with `pd.read_csv`.
+- ProtWise phosphosite and RNA result inputs currently expect Excel workbooks.
+- Networkmodel kinase network, TF network, MS/protein data, RNA data, and phosphoproteomics inputs currently expect CSV because the backend reads them with default `pd.read_csv`.
+- Networkmodel previous KinOpt/TFOpt result priors remain Excel workbooks.
+
+## Running workflows
+
+The dashboard-generated command is shown before execution. These are representative commands using actual Pixi task names from `pixi.toml`:
+
+```bash
+pixi run kinopt-local --outdir results/example_kinopt
+```
+
+```bash
+pixi run tfopt-local --outdir results/example_tfopt
+```
+
+```bash
+pixi run model --conf config.toml --outdir results/example_protwise
+```
+
+```bash
+pixi run networkmodel --conf config.toml --output-dir results/example_networkmodel
+```
+
+Additional actual tasks include:
+
+```bash
+pixi run prep
+pixi run kinopt-evol
+pixi run tfopt-evol
+pixi run phoskintime-all
+```
+
+Use long-running workflows with care. Networkmodel and full integrated runs can take substantially longer than local parsing or result browsing.
+
+## Command preview
+
+The dashboard shows the exact command and argument list before running because:
+
+- users can copy/paste it into a terminal;
+- the command can be recorded in `command.txt`;
+- it makes file paths and config choices visible;
+- it prevents hidden free-text shell execution.
+
+Commands are built as argument lists, not shell strings.
+
+## Result interpretation
+
+Common output groups:
+
+| Folder | Meaning |
+| --- | --- |
+| `tables/` | CSV, TSV, Excel, JSON, NumPy, or other table-like outputs. |
+| `plots/` | Static and interactive plots. |
+| `logs/` | Auxiliary logs beyond the root `console.log`. |
+| `reports/` | HTML, Markdown, or PDF reports. |
+| `artifacts/` | Pickles, serialized bundles, intermediate arrays, and files that are useful for reloading or debugging. |
+
+Workflow-specific tabs add convenience views for KinOpt, TFOpt, ProtWise, Networkmodel, and advanced analyses when recognized files are present.
+
+## Downloading results
+
+Open the **Download** tab in a selected result directory and click **Prepare ZIP archive**. The ZIP is generated in memory and served to the browser. It is not written into the repository by default.
+
+## Reproducibility
+
+A dashboard-ready run should contain:
+
+- `metadata.json` — workflow identity, timestamps, environment details, config provenance, input hashes where practical, and selected parameters.
+- `command.txt` — exact or reconstructed command invocation.
+- `console.log` — stdout/stderr or logger output.
+- `config_resolved.yaml` or `config_resolved.toml` — effective resolved configuration after defaults and CLI overrides.
+
+Config precedence is:
+
+```text
+explicit CLI/dashboard field
+> custom --conf
+> repository config.toml
+> hard-coded fallback only where unavoidable
+```
+
+## Known limitations
+
+- Long-running jobs can take minutes or hours.
+- Large result folders may load slowly, especially large HTML reports or many images.
+- Optional visualization dependencies may be missing in minimal environments.
+- The dashboard does not replace CLI/API workflows.
+- The dashboard does not change scientific model assumptions.
+- Workflow validation should match backend readers; if a backend cannot read TSV/XLSX, the dashboard should not permit those formats for execution.
diff --git a/docs/index.md b/docs/index.md
index 45e5572..9a8da8e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -75,6 +75,11 @@ Executable educational notebooks are available under `notebooks/` and cover end-
Run them interactively with `jupyter lab notebooks/` or as executable tests with `pytest --nbmake notebooks/*.ipynb`.
+
+## No-code dashboard
+
+Use the unified Streamlit dashboard to browse existing result directories, preview uploaded inputs, launch registered CLI/Pixi workflows, and download result archives. See [No-code dashboard documentation](Documentation/no_code_dashboard.md).
+
## Modeling backends (local vs global)
PhosKinTime provides two complementary modeling stacks:
diff --git a/docs/result_directory_contract.md b/docs/result_directory_contract.md
new file mode 100644
index 0000000..2651926
--- /dev/null
+++ b/docs/result_directory_contract.md
@@ -0,0 +1,149 @@
+# Result directory contract
+
+PhosKinTime workflows should write outputs into a predictable result directory so the dashboard, tests, and downstream tools can parse runs without workflow-specific guesses.
+
+## Standard layout
+
+```text
+results//
+├── metadata.json
+├── command.txt
+├── console.log
+├── config_resolved.yaml
+├── tables/
+├── plots/
+├── logs/
+├── reports/
+└── artifacts/
+```
+
+`config_resolved.toml` is also acceptable when a workflow naturally writes TOML. Current shared helpers write `config_resolved.yaml`.
+
+## Files and folders
+
+| Path | Required? | Purpose | Expected formats | Produced by | Dashboard use |
+| --- | --- | --- | --- | --- | --- |
+| `metadata.json` | Required for new runs | Machine-readable provenance: workflow, environment, config, inputs, status, settings. | JSON | CLI wrappers, dashboard runner, backend runners | Rendered in Metadata tab; used to infer workflow. |
+| `command.txt` | Required for new runs | Exact or reconstructed invocation. | Plain text | CLI wrappers and dashboard runner | Rendered as command provenance. |
+| `console.log` | Required when run through CLI/dashboard logging | Combined stdout/stderr or logger output. | Text/log | CLI wrappers and dashboard runner | Rendered in Logs tab and failure tail. |
+| `config_resolved.yaml` / `config_resolved.toml` | Required when config/defaults are used | Effective config after default resolution and CLI overrides. | YAML/TOML/JSON-compatible YAML | Config-aware workflows | Rendered in Metadata tab for reproducibility. |
+| `tables/` | Required folder, contents optional | Tabular outputs and mirrored legacy tables. | `.csv`, `.tsv`, `.xlsx`, `.xls`, `.json`, `.npy`, `.npz`, `.parquet` | All workflows as applicable | Listed lazily; shown with `st.dataframe` where supported. |
+| `plots/` | Required folder, contents optional | Static and interactive figures. | `.png`, `.jpg`, `.jpeg`, `.svg`, `.html`, sometimes `.pdf` | Optimization, fitting, analysis workflows | Displayed with image/HTML viewers. |
+| `logs/` | Required folder, contents optional | Auxiliary logs beyond root `console.log`. | Text/log files | Workflows and analysis scripts | Listed in Logs tab. |
+| `reports/` | Required folder, contents optional | Human-readable reports. | `.html`, `.md`, `.pdf` | ProtWise, Networkmodel, analysis scripts | Rendered when possible; downloadable. |
+| `artifacts/` | Required folder, contents optional | Serialized states and non-table artifacts. | `.pkl`, `.pickle`, `.joblib`, arbitrary files | Networkmodel, analyses, dashboard bundles | Downloadable; some workflow panels may parse known artifacts. |
+
+## Metadata schema
+
+New producers should include at least the following conceptual fields. Names may differ slightly for legacy runs, but dashboard-facing writers should prefer these stable keys when possible.
+
+```json
+{
+ "workflow": "networkmodel",
+ "run_id": "example",
+ "timestamp_start": "...",
+ "timestamp_end": "...",
+ "status": "success",
+ "return_code": 0,
+ "command": ["pixi", "run", "..."],
+ "config_path": "config.toml",
+ "custom_config_used": false,
+ "output_dir": "results/example",
+ "python_version": "...",
+ "package_version": "...",
+ "git_commit": "...",
+ "inputs": {},
+ "parameters": {}
+}
+```
+
+Current shared metadata helpers also record fields such as `command_arguments`, `output_directory`, `python_executable`, `pixi_environment`, input hashes, and workflow-specific effective settings.
+
+Recommended metadata details:
+
+- Use paths relative to the repository root where possible.
+- Include resolved absolute paths only when useful for debugging local runs.
+- Include file hashes for user inputs where practical.
+- Record config priority outcomes: supplied `--conf`, resolved config path, default/custom config source, and effective settings.
+- Record status and return code for dashboard-launched subprocesses.
+
+## Config precedence
+
+Workflow runners should resolve settings in this order:
+
+```text
+explicit CLI/dashboard field
+> custom --conf
+> repository config.toml
+> hard-coded fallback only where unavoidable
+```
+
+`config_resolved.*` should represent the effective configuration after this precedence is applied.
+
+## Legacy layout support
+
+The dashboard result parser recognizes selected legacy outputs so older runs can still be browsed. Known examples include:
+
+- Networkmodel top-level `scalar_objective.csv`, `convergence_history.csv`, `pred_prot_picked.csv`, `pred_rna_picked.csv`, `pred_phospho_picked.csv`.
+- Networkmodel `optimization/`, `profiles/`, `posterior/`, and `plots/` folders.
+- Legacy `kinopt_results.xlsx` and `tfopt_results.xlsx` workbooks.
+- Top-level `report.html` where present.
+
+Legacy support is compatibility behavior. New workflow code should still write the standard folders and provenance files.
+
+Integrated `phoskintime-all` runs may contain child workflow result folders such as `tfopt/`, `kinopt/`, and `protwise/` beneath the parent run directory. The parent directory records launcher-level provenance, while each child directory remains independently browseable with its own tables, plots, reports, logs, artifacts, and provenance where produced.
+
+## Workflow-specific expected outputs
+
+### KinOpt
+
+Expected outputs include:
+
+- `kinopt_results.xlsx` or a mirrored copy under `tables/`;
+- alpha/beta or equivalent parameter tables;
+- observed-vs-estimated sheets/tables when available;
+- fit and diagnostic plots under `plots/`;
+- logs and provenance files.
+
+### TFOpt
+
+Expected outputs include:
+
+- `tfopt_results.xlsx` or a mirrored copy under `tables/`;
+- TF alpha/beta or equivalent tables;
+- latent activity, dominance, knockout, or fit outputs when generated;
+- plots and reports under standard folders;
+- logs and provenance files.
+
+### ProtWise
+
+Expected outputs include:
+
+- protein-wise ODE fit workbook/tables;
+- residuals, fitted predictions, sensitivity outputs when generated;
+- goodness-of-fit and diagnostic plots;
+- `report.html` under `reports/` or mirrored from top level;
+- provenance and resolved config.
+
+### Networkmodel
+
+Expected outputs include:
+
+- scalar objective/convergence CSVs;
+- picked protein/RNA/phospho prediction CSVs;
+- `dashboard_bundle.pkl` and optimization artifacts;
+- optimization/profile/posterior subfolders;
+- fit plots and steady-state/network plots;
+- provenance and resolved config.
+
+### Scripts and advanced analyses
+
+Advanced analysis scripts should write into the selected run directory:
+
+- tables to `tables/`;
+- figures to `plots/`;
+- narrative outputs to `reports/`;
+- serialized or miscellaneous outputs to `artifacts/`;
+- logs to `logs/` or root `console.log` when run through the dashboard runner.
+
+Scripts should not write into input data folders unless an explicit, documented export action requests it.
diff --git a/docs/workflow_cli_reference.md b/docs/workflow_cli_reference.md
new file mode 100644
index 0000000..d676c70
--- /dev/null
+++ b/docs/workflow_cli_reference.md
@@ -0,0 +1,294 @@
+# Workflow CLI reference
+
+This reference lists actual Pixi tasks and Typer wrapper commands present in the repository. The CLI remains the source of truth for workflow execution; the dashboard builds and previews these commands.
+
+## Config precedence
+
+For workflows that support `--conf`, the dashboard currently validates assigned config files as TOML because the target runners parse TOML:
+
+```text
+Explicit CLI flags override values from --conf.
+Values from --conf override repository defaults.
+If --conf is not supplied, repository config.toml is used.
+```
+
+Do not assume a custom config was used unless `metadata.json` and `config_resolved.*` confirm it.
+
+## Pixi tasks
+
+| Task | Purpose | Backend command/module | Example |
+| --- | --- | --- | --- |
+| `prep` | Preprocessing cleanup. | `python -m processing.cleanup` | `pixi run prep` |
+| `kinopt-local` | Local KinOpt. | `python -m kinopt.local` | `pixi run kinopt-local --outdir results/example_kinopt` |
+| `kinopt-evol` | Evolutionary KinOpt. | `python -m kinopt.evol` | `pixi run kinopt-evol` |
+| `kinopt-fitanalysis` | KinOpt fit analysis. | `python -m kinopt.fitanalysis` | `pixi run kinopt-fitanalysis` |
+| `tfopt-local` | Local TFOpt. | `python -m tfopt.local` | `pixi run tfopt-local --outdir results/example_tfopt` |
+| `tfopt-evol` | Evolutionary TFOpt. | `python -m tfopt.evol` | `pixi run tfopt-evol` |
+| `model` | ProtWise model runner. | `python -m protwise.runner.main` | `pixi run model --conf config.toml --outdir results/example_protwise` |
+| `networkmodel` | Global network model runner. | `python -m networkmodel.runner` | `pixi run networkmodel --conf config.toml --output-dir results/example_networkmodel` |
+| `phoskintime` | Typer workflow wrapper. | `python -m phoskintime.config.cli` | `pixi run phoskintime --help` |
+| `phoskintime-all` | Typer wrapper `all` command. | `python -m phoskintime.config.cli all` | `pixi run phoskintime-all` |
+| `network-dashboard` | Legacy Networkmodel dashboard launcher. | `python run_dashboard.py` | `pixi run network-dashboard` |
+| `dashboard` | Unified no-code dashboard. | `streamlit run dashboard/app.py` | `pixi run dashboard` |
+| `dashboard-dev` | Unified dashboard with reload-on-save. | `streamlit run dashboard/app.py --server.runOnSave=true` | `pixi run dashboard-dev` |
+| `test` | Run pytest with project path. | `PYTHONPATH=.:.. pytest` | `pixi run -e dev test` |
+| `test-notebooks` | Execute notebooks as tests. | `pytest --nbmake notebooks/*.ipynb` | `pixi run -e dev test-notebooks` |
+| `test-cov` | Coverage test run. | `pytest --cov=...` | `pixi run -e dev test-cov` |
+| `check-import` | Import smoke check. | Python one-liner | `pixi run check-import` |
+| `docs-serve` | Serve documentation. | `zensical serve` | `pixi run -e docs docs-serve` |
+| `docs-build` | Build documentation. | `zensical build` | `pixi run -e docs docs-build` |
+
+## Typer wrapper commands (`config/cli.py`)
+
+These commands are available through the package CLI wrapper when invoked as the configured module.
+
+### `prep`
+
+- Purpose: run preprocessing cleanup.
+- Backend module: `processing.cleanup`.
+- Required inputs: project preprocessing inputs.
+- Config support: no explicit `--conf` on wrapper command.
+- Output directory flag: none.
+- Example:
+
+```bash
+pixi run phoskintime prep
+```
+
+Dashboard integration: registered as `prep`.
+
+### `kinopt`
+
+- Purpose: run KinOpt local or evolutionary mode.
+- Backend module: `kinopt.`.
+- Required inputs: KinOpt inputs configured in `config.toml`/backend constants.
+- Config support: `--conf`.
+- Output directory flag: `--outdir` / `--output-dir` for local mode.
+- Optional arguments: `--mode local|evol`.
+- Example:
+
+```bash
+pixi run phoskintime kinopt --mode local --conf config.toml --outdir results/example_kinopt
+```
+
+Dashboard integration: local mode registered as `kinopt-local`.
+
+### `tfopt`
+
+- Purpose: run TFOpt local or evolutionary mode.
+- Backend module: `tfopt.`.
+- Required inputs: TFOpt inputs configured in `config.toml`/backend constants.
+- Config support: `--conf`.
+- Output directory flag: `--outdir` / `--output-dir` for local mode.
+- Optional arguments: `--mode local|evol`.
+- Example:
+
+```bash
+pixi run phoskintime tfopt --mode local --conf config.toml --outdir results/example_tfopt
+```
+
+Dashboard integration: local mode registered as `tfopt-local`.
+
+### `model`
+
+- Purpose: run ProtWise protein-wise ODE fitting.
+- Backend module: `protwise.runner.main`.
+- Required inputs: protein CSV, phosphosite Excel workbook, RNA/TFOpt Excel workbook.
+- Config support: `--conf`; custom config values are used unless overridden by explicit CLI fields.
+- Output directory flag: `--outdir` / `--output-dir`.
+- Example:
+
+```bash
+pixi run phoskintime model --conf config.toml --outdir results/example_protwise
+```
+
+Dashboard integration: registered as `protwise-model` / Pixi task `model`.
+
+### `networkmodel`
+
+- Purpose: run the integrated global network model.
+- Backend module: `networkmodel.runner`.
+- Required inputs: kinase network CSV, TF network CSV, MS/protein CSV, RNA CSV, optional phosphoproteomics CSV, optional KinOpt/TFOpt Excel priors.
+- Config support: `--conf`; custom config values are used unless overridden by explicit CLI fields.
+- Output directory flag: `--outdir` / `--output-dir`; dashboard uses `--output-dir`.
+- Example:
+
+```bash
+pixi run phoskintime networkmodel --conf config.toml --outdir results/example_networkmodel
+```
+
+Dashboard integration: registered as `networkmodel`.
+
+### `all`
+
+- Purpose: run preprocessing, TFOpt, KinOpt, and ProtWise model stages in sequence.
+- Backend modules: `processing.cleanup`, `tfopt.`, `kinopt.`, `protwise.runner.main`.
+- Config support: `--tf-conf`, `--kin-conf`, `--model-conf`.
+- Output directory flag: `--outdir` / `--output-dir` as a base directory for stage outputs.
+- Example:
+
+```bash
+pixi run phoskintime-all --outdir results/example_all
+```
+
+Dashboard integration: registered as `phoskintime-all`.
+
+### `clean`
+
+- Purpose: remove bytecode, cache, and build artifacts.
+- Backend: filesystem cleanup in `config/cli.py`.
+- Config support: none.
+- Output directory flag: none.
+- Example:
+
+```bash
+pixi run phoskintime clean
+```
+
+Dashboard integration: not a dashboard workflow.
+
+## Workflow details
+
+### Preprocessing / `prep`
+
+- Command name: `prep`.
+- Purpose: cleanup/preprocessing.
+- Backend module: `processing.cleanup`.
+- Required inputs: repository raw/processing inputs.
+- Optional arguments: none in the Pixi task.
+- Config file support: not exposed by task.
+- Output directory flag: none.
+- Expected outputs: preprocessing outputs defined by `processing.cleanup`.
+- Dashboard status: executable from dashboard registry; result browsing is limited unless outputs follow the result contract.
+
+### KinOpt local
+
+- Command name: `kinopt-local`.
+- Purpose: local kinase/phosphosite optimization.
+- Backend module: `kinopt.local`.
+- Required inputs: configured KinOpt input files, commonly CSV files under `data/`.
+- Optional arguments include `--conf`, `--lower_bound`, `--upper_bound`, `--loss_type`, and `--method` where supported.
+- Output directory flag: `--outdir` / `--output-dir`.
+- Expected outputs: `kinopt_results.xlsx`, tables, plots, reports, logs, provenance.
+- Dashboard status: executable and result-browsable.
+
+### KinOpt evol
+
+- Command name: `kinopt-evol`.
+- Purpose: evolutionary KinOpt optimization.
+- Backend module: `kinopt.evol`.
+- Inputs/config: backend-defined.
+- Output directory flag: not dashboard-standardized in the Pixi task.
+- Dashboard status: not directly registered for dashboard execution; produced folders can be browsed if layout is recognized.
+
+### TFOpt local
+
+- Command name: `tfopt-local`.
+- Purpose: local TF/mRNA optimization.
+- Backend module: `tfopt.local`.
+- Required inputs: configured TFOpt files, commonly CSV files under `data/`.
+- Optional arguments include `--conf`, `--lower_bound`, `--upper_bound`, and `--loss_type` where supported.
+- Output directory flag: `--outdir` / `--output-dir`.
+- Expected outputs: `tfopt_results.xlsx`, tables, plots, reports, logs, provenance.
+- Dashboard status: executable and result-browsable.
+
+### TFOpt evol
+
+- Command name: `tfopt-evol`.
+- Purpose: evolutionary TFOpt optimization.
+- Backend module: `tfopt.evol`.
+- Inputs/config: backend-defined.
+- Output directory flag: not dashboard-standardized in the Pixi task.
+- Dashboard status: not directly registered for dashboard execution; produced folders can be browsed if layout is recognized.
+
+### ProtWise / model
+
+- Command name: `model`.
+- Purpose: protein-wise ODE fitting.
+- Backend module: `protwise.runner.main`.
+- Required inputs: protein CSV, phosphosite Excel workbook, RNA/TFOpt Excel workbook.
+- Config support: `--conf`.
+- Output directory flag: `--outdir` / `--output-dir`.
+- Example:
+
+```bash
+pixi run model --conf config.toml --outdir results/example_protwise
+```
+
+- Expected outputs: result workbook, plots, report, logs, provenance, standard folders.
+- Dashboard status: executable and result-browsable.
+
+### Networkmodel
+
+- Command name: `networkmodel`.
+- Purpose: global coupled signaling/GRN model.
+- Backend module: `networkmodel.runner`.
+- Required inputs: kinase network CSV, TF network CSV, MS/protein CSV, RNA CSV, optional phosphoproteomics CSV; optional prior workbooks from KinOpt/TFOpt.
+- Config support: `--conf`.
+- Output directory flag: `--output-dir` / `--outdir`.
+- Example:
+
+```bash
+pixi run networkmodel --conf config.toml --output-dir results/example_networkmodel
+```
+
+- Expected outputs: scalar objective/convergence CSVs, predictions, `dashboard_bundle.pkl`, plots, optimization/profile/posterior outputs, logs, provenance.
+- Dashboard status: executable and result-browsable.
+
+### Dashboard
+
+- Commands: `dashboard`, `dashboard-dev`, `network-dashboard`.
+- Purpose: `dashboard` and `dashboard-dev` run the unified dashboard; `network-dashboard` runs the legacy Networkmodel dashboard.
+- Backend modules: `dashboard/app.py` or `run_dashboard.py`.
+- Example:
+
+```bash
+pixi run dashboard
+pixi run dashboard-dev
+```
+
+### Tests and docs
+
+- Test command:
+
+```bash
+pixi run -e dev test
+```
+
+- Focused dashboard tests:
+
+```bash
+pixi run -e dev pytest tests/dashboard -v
+```
+
+- Docs build:
+
+```bash
+pixi run -e docs docs-build
+```
+
+## Standalone analysis scripts
+
+The `scripts/` directory contains standalone utilities, including:
+
+- `analyze_tf_kin_counts.py`
+- `compare_mechanisms.py`
+- `curve_similarity.py`
+- `export_subnetworks.py`
+- `find_protein_accumulators.py`
+- `kinopt_network_readout.py`
+- `kinopt_network_viz.py`
+- `make_kinopt_diagram.py`
+- `mechanistic_insights.py`
+- `temporal_sensitivity.py`
+- `tfopt_network_readout.py`
+- `tfopt_network_viz.py`
+
+These are not all Pixi tasks. Run them directly only after reading each script's arguments and expected inputs. When integrating a script into the dashboard, parameterize input/output paths and write outputs into the selected run directory under `tables/`, `plots/`, `reports/`, or `artifacts/`.
+
+## File-format constraints
+
+- Do not document YAML/JSON execution config support unless the target runner implements YAML/JSON loading; dashboard `--conf` assignments are restricted to `.toml`.
+- Do not document TSV/XLSX execution support for Networkmodel network/data inputs unless the backend reader is extended beyond default `pandas.read_csv`.
+- Do not document Excel support for ProtWise protein input unless the backend reader changes from `pd.read_csv`.
+- Dashboard upload preview can read more formats than a specific workflow can execute.
diff --git a/global_model/__init__.py b/global_model/__init__.py
new file mode 100644
index 0000000..f577501
--- /dev/null
+++ b/global_model/__init__.py
@@ -0,0 +1,31 @@
+"""Backward-compatibility package for old pickled dashboard bundles.
+
+Old dashboard_bundle.pkl files may reference modules such as:
+ global_model.network
+ global_model.params
+ global_model.simulate
+These have moved under networkmodel.*.
+"""
+
+from __future__ import annotations
+
+import importlib
+import sys
+
+_MODULE_ALIASES = {
+ "global_model.network": "networkmodel.network",
+ "global_model.params": "networkmodel.params",
+ "global_model.simulate": "networkmodel.simulate",
+ "global_model.utils": "networkmodel.utils",
+ "global_model.backend": "networkmodel.backend",
+ "global_model.export": "networkmodel.export",
+ "global_model.optproblem": "networkmodel.OptimizationProblem",
+ "global_model.OptimizationProblem": "networkmodel.OptimizationProblem",
+ "global_model.dashboard_bundle": "networkmodel.dashboard_bundle",
+}
+
+for old_name, new_name in _MODULE_ALIASES.items():
+ try:
+ sys.modules.setdefault(old_name, importlib.import_module(new_name))
+ except ModuleNotFoundError:
+ pass
diff --git a/kinopt/local/__main__.py b/kinopt/local/__main__.py
index fa09afb..2bf60f2 100644
--- a/kinopt/local/__main__.py
+++ b/kinopt/local/__main__.py
@@ -1,7 +1,6 @@
-import shutil
from functools import partial
-from kinopt.local.config.constants import parse_args, OUT_DIR, OUT_FILE, ODE_DATA_DIR
+from kinopt.local.config.constants import parse_args, OUT_FILE, INPUT1, INPUT2, _CFG
from kinopt.local.config.helpers import location
from kinopt.local.exporter.plotout import export_outcomes_to_csv, plot_multistart_summary_runtime_overlay
from kinopt.local.exporter.sheetutils import output_results, export_params_npz
@@ -16,6 +15,10 @@
from kinopt.optimality.KKT import post_optimization_results
from kinopt.fitanalysis import optimization_performance
from common.utils import latexit
+from common.results import (
+ attach_file_console_logger, ensure_result_dir, populate_standard_subdirs,
+ write_command, write_metadata, write_resolved_config,
+)
logger = setup_logger()
@@ -33,7 +36,23 @@ def main():
check_kinases()
# Parse arguments.
- lb, ub, loss_type, estimate_missing, scaling_method, split_point, seg_points, opt_method = parse_args()
+ lb, ub, loss_type, estimate_missing, scaling_method, split_point, seg_points, opt_method, out_dir = parse_args()
+ result_dirs = ensure_result_dir(out_dir)
+ out_dir = result_dirs["root"]
+ out_file = out_dir / OUT_FILE.name
+ attach_file_console_logger(logger, out_dir)
+ write_command(out_dir)
+ write_resolved_config(out_dir, _CFG)
+ write_metadata(
+ out_dir,
+ workflow="kinopt.local",
+ args={
+ "lower_bound": lb, "upper_bound": ub, "loss_type": loss_type,
+ "estimate_missing_kinases": estimate_missing, "scaling_method": scaling_method,
+ "split_point": split_point, "segment_points": seg_points, "method": opt_method,
+ },
+ inputs=[INPUT1, INPUT2],
+ )
# Load and scale data.
full_df, interact_df, _ = load_and_scale_data(estimate_missing, scaling_method, split_point, seg_points)
@@ -107,16 +126,16 @@ def main():
# Save outcomes
export_outcomes_to_csv(
outcomes,
- OUT_DIR / "multistart_summary.csv"
+ out_dir / "multistart_summary.csv"
)
# Save optimized parameters for each start
- export_params_npz(outcomes, OUT_DIR / "multistart_params.npz")
+ export_params_npz(outcomes, out_dir / "multistart_params.npz")
# Save runtime vs objective function value plot
plot_multistart_summary_runtime_overlay(
- OUT_DIR / "multistart_summary.csv",
- out_path=OUT_DIR / "multistart_fun_vs_rank_runtime.png",
+ out_dir / "multistart_summary.csv",
+ out_path=out_dir / "multistart_fun_vs_rank_runtime.png",
figsize=(8, 8),
)
@@ -133,28 +152,36 @@ def main():
# Output results.
output_results(P_initial, P_init_dense, P_estimated, residuals, alpha_values, beta_values,
- result, mse, rmse, mae, mape, r_squared)
-
- # Copy output file to ODE data directory.
- shutil.copy(OUT_FILE, ODE_DATA_DIR / OUT_FILE.name)
-
- # Analyze optimization performance.
+ result, mse, rmse, mae, mape, r_squared, filename=out_file, out_dir=out_dir)
+
+
+ # Analyze optimization performance using the selected result directory.
+ import kinopt.optimality.KKT as kkt_module
+ import kinopt.fitanalysis.__main__ as fitanalysis_module
+ import kinopt.fitanalysis.helpers.postfit as postfit_module
+ kkt_module.OUT_FILE = out_file
+ kkt_module.OUT_DIR = out_dir
+ fitanalysis_module.OUT_FILE = out_file
+ fitanalysis_module.OUT_DIR = out_dir
+ postfit_module.OUT_DIR = out_dir
post_optimization_results()
optimization_performance()
# LateX the results.
- latexit.main(OUT_DIR)
+ latexit.main(out_dir)
# Organize output files and create a report.
- organize_output_files(OUT_DIR)
- create_report(OUT_DIR)
+ organize_output_files(out_dir)
+ create_report(out_dir)
- logger.info(f'Report & Results {location(str(OUT_DIR))}')
+ logger.info(f'Report & Results {location(str(out_dir))}')
# Click to open the report in a web browser.
- for fpath in [OUT_DIR / 'report.html']:
+ for fpath in [out_dir / 'report.html']:
logger.info(f"{fpath.as_uri()}")
+ populate_standard_subdirs(out_dir)
+
if __name__ == "__main__":
main()
diff --git a/kinopt/local/config/constants.py b/kinopt/local/config/constants.py
index c2dcbb6..32dc1cf 100644
--- a/kinopt/local/config/constants.py
+++ b/kinopt/local/config/constants.py
@@ -45,6 +45,7 @@ def parse_args():
description="PhosKinTime - SLSQP/TRUST-CONSTR Kinase Phosphorylation Optimization Problem prior to ODE Modelling."
)
+ parser.add_argument("--conf", default=None, help="Compatibility option; configuration is loaded before argument parsing.")
parser.add_argument("--lower_bound", type=float, default=float(_CFG.get("lower_bound", -4.0)))
parser.add_argument("--upper_bound", type=float, default=float(_CFG.get("upper_bound", 4.0)))
@@ -92,6 +93,13 @@ def parse_args():
default=str(_CFG.get("method", "slsqp")),
help="Optimization method.",
)
+ parser.add_argument(
+ "--outdir", "--output-dir",
+ dest="outdir",
+ type=Path,
+ default=OUT_DIR,
+ help="Directory where all run outputs and provenance files are written.",
+ )
args = parser.parse_args()
@@ -107,4 +115,5 @@ def parse_args():
args.split_point,
seg_points,
args.method,
+ args.outdir,
)
diff --git a/kinopt/local/exporter/plotout.py b/kinopt/local/exporter/plotout.py
index 1ab5a72..92bfd75 100644
--- a/kinopt/local/exporter/plotout.py
+++ b/kinopt/local/exporter/plotout.py
@@ -36,7 +36,7 @@ def format_timepoints(tp, tol=1e-9):
labels.append(f"{x:.1f}")
return labels
-def plot_fits_for_gene(gene, gene_data, real_timepoints):
+def plot_fits_for_gene(gene, gene_data, real_timepoints, out_dir=OUT_DIR):
"""
Function to plot the observed and estimated phosphorylation levels for each psite of a gene.
@@ -86,7 +86,7 @@ def plot_fits_for_gene(gene, gene_data, real_timepoints):
axs[1].set_xticklabels(format_timepoints(xt))
plt.tight_layout()
- filename = f"{OUT_DIR}/{gene}_fit_.png"
+ filename = Path(out_dir) / f"{gene}_fit_.png"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
@@ -120,7 +120,7 @@ def export_outcomes_to_csv(outcomes, csv_path):
writer.writeheader()
writer.writerows(rows)
-def plot_cumulative_residuals(gene, gene_data, real_timepoints):
+def plot_cumulative_residuals(gene, gene_data, real_timepoints, out_dir=OUT_DIR):
"""
Function to plot the cumulative residuals for each psite of a gene.
@@ -142,12 +142,12 @@ def plot_cumulative_residuals(gene, gene_data, real_timepoints):
plt.grid(True, alpha=0.2)
plt.legend(title="Residue_Position")
plt.tight_layout()
- filename = f"{OUT_DIR}/{gene}_cumulative_residuals_.png"
+ filename = Path(out_dir) / f"{gene}_cumulative_residuals_.png"
plt.savefig(filename, format='png', dpi=300)
plt.close()
-def plot_autocorrelation_residuals(gene, gene_data, real_timepoints):
+def plot_autocorrelation_residuals(gene, gene_data, real_timepoints, out_dir=OUT_DIR):
"""
Function to plot the autocorrelation of residuals for each psite of a gene.
@@ -164,12 +164,12 @@ def plot_autocorrelation_residuals(gene, gene_data, real_timepoints):
plt.xlabel("Lags")
plt.ylabel("Autocorrelation")
plt.tight_layout()
- filename = f"{OUT_DIR}/{gene}_autocorrelation_residuals_.png"
+ filename = Path(out_dir) / f"{gene}_autocorrelation_residuals_.png"
plt.savefig(filename, format='png', dpi=300)
plt.close()
-def plot_histogram_residuals(gene, gene_data, real_timepoints):
+def plot_histogram_residuals(gene, gene_data, real_timepoints, out_dir=OUT_DIR):
"""
Function to plot histograms of residuals for each psite of a gene.
@@ -190,12 +190,12 @@ def plot_histogram_residuals(gene, gene_data, real_timepoints):
plt.grid(True, alpha=0.2)
plt.legend(title="Residue_Position")
plt.tight_layout()
- filename = f"{OUT_DIR}/{gene}_histogram_residuals_.png"
+ filename = Path(out_dir) / f"{gene}_histogram_residuals_.png"
plt.savefig(filename, format='png', dpi=300)
plt.close()
-def plot_qqplot_residuals(gene, gene_data, real_timepoints):
+def plot_qqplot_residuals(gene, gene_data, real_timepoints, out_dir=OUT_DIR):
"""
Function to plot QQ plots of residuals for each psite of a gene.
@@ -209,7 +209,7 @@ def plot_qqplot_residuals(gene, gene_data, real_timepoints):
qqplot(gene_data["residuals"][i], line='s', ax=plt.gca())
plt.title(f"{gene}")
plt.tight_layout()
- filename = f"{OUT_DIR}/{gene}_qqplot_residuals_.png"
+ filename = Path(out_dir) / f"{gene}_qqplot_residuals_.png"
plt.savefig(filename, format='png', dpi=300)
plt.close('all')
diff --git a/kinopt/local/exporter/sheetutils.py b/kinopt/local/exporter/sheetutils.py
index a6dd5a9..39f17b2 100644
--- a/kinopt/local/exporter/sheetutils.py
+++ b/kinopt/local/exporter/sheetutils.py
@@ -1,5 +1,5 @@
import pandas as pd
-from kinopt.local.config.constants import TIME_POINTS, OUT_FILE
+from kinopt.local.config.constants import TIME_POINTS, OUT_FILE, OUT_DIR
from kinopt.local.exporter.helpers import build_genes_data
from kinopt.local.exporter.plotout import *
from kinopt.local.config.logconf import setup_logger
@@ -8,7 +8,7 @@
def output_results(P_initial, P_init_dense, P_estimated, residuals, alpha_values, beta_values,
- result, mse, rmse, mae, mape, r_squared):
+ result, mse, rmse, mae, mape, r_squared, filename=OUT_FILE, out_dir=OUT_DIR):
"""
Function to output the results of the optimization process.
@@ -53,14 +53,14 @@ def output_results(P_initial, P_init_dense, P_estimated, residuals, alpha_values
# For each gene, call the plotting functions.
for gene, data in genes_data.items():
- plot_fits_for_gene(gene, data, TIME_POINTS)
- plot_cumulative_residuals(gene, data, TIME_POINTS)
- plot_autocorrelation_residuals(gene, data, TIME_POINTS)
- plot_histogram_residuals(gene, data, TIME_POINTS)
- plot_qqplot_residuals(gene, data, TIME_POINTS)
+ plot_fits_for_gene(gene, data, TIME_POINTS, out_dir=out_dir)
+ plot_cumulative_residuals(gene, data, TIME_POINTS, out_dir=out_dir)
+ plot_autocorrelation_residuals(gene, data, TIME_POINTS, out_dir=out_dir)
+ plot_histogram_residuals(gene, data, TIME_POINTS, out_dir=out_dir)
+ plot_qqplot_residuals(gene, data, TIME_POINTS, out_dir=out_dir)
# Write results to Excel.
- output_file = OUT_FILE
+ output_file = filename
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
alpha_list = []
for (gene, psite), kinases in alpha_values.items():
diff --git a/mkdocs.yml b/mkdocs.yml
index 57adbc4..67a3c9d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -35,8 +35,10 @@ theme:
nav:
- Home: index.md
- - Reference: reference.md
- - Changelog: Documentation/CHANGELOG.md
+ - Terms of use: ../TERMS_OF_USE.md
+ - Contributing: ../CONTRIBUTING.md
+ - API Reference: reference.md
+ - Changelog: ../CHANGELOG.md
- Documentation:
- Architecture & Data Flow: Documentation/architecture.md
- Configuration Reference: Documentation/configuration.md
@@ -54,6 +56,12 @@ nav:
- Plotting: Documentation/plotting/README.md
- Utils: Documentation/utils/README.md
- Dashboard: Documentation/dashboard.md
+ - Dashboard Quickstart: dashboard_quickstart.md
+ - Dashboard User Guide: dashboard_user_guide.md
+ - Dashboard Developer Guide: dashboard_developer_guide.md
+ - Dashboard Troubleshooting: dashboard_troubleshooting.md
+ - Result Directory Contract: result_directory_contract.md
+ - Workflow CLI Reference: workflow_cli_reference.md
- Scripts: Documentation/scripts.md
- Docker: Documentation/docker.md
- Testing: Documentation/testing.md
diff --git a/networkmodel/BayesianInference.py b/networkmodel/BayesianInference.py
index d21f0ee..163be36 100644
--- a/networkmodel/BayesianInference.py
+++ b/networkmodel/BayesianInference.py
@@ -9,7 +9,6 @@
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, replace
import json
-import multiprocessing as mp
import os
import sys
from pathlib import Path
@@ -312,14 +311,15 @@ def _plot_multistart(summary: pd.DataFrame, params: pd.DataFrame, names: Sequenc
fig.savefig(plot_dir / "parameter_objective_tradeoff.png", dpi=300)
plt.close(fig)
+
def run_profile_likelihood_standalone_processes(
- *,
- run_config_path: str | Path,
- output_dir: str | Path,
- parameter_indices: Sequence[int],
- grid_size: int = 5,
- max_workers: int = 1,
- timeout_seconds: int | None = None,
+ *,
+ run_config_path: str | Path,
+ output_dir: str | Path,
+ parameter_indices: Sequence[int],
+ grid_size: int = 5,
+ max_workers: int = 1,
+ timeout_seconds: int | None = None,
) -> dict:
"""Run profile likelihood in standalone subprocesses.
@@ -480,7 +480,7 @@ def _start_worker(param_idx: int):
.transform("min")
)
summary.loc[finite_mask, "delta_objective"] = (
- summary.loc[finite_mask, "objective_value"].to_numpy() - mins.to_numpy()
+ summary.loc[finite_mask, "objective_value"].to_numpy() - mins.to_numpy()
)
summary.to_csv(out / "profile_likelihood_summary.csv", index=False)
@@ -506,6 +506,7 @@ def _start_worker(param_idx: int):
"output_dir": out,
}
+
def run_profile_likelihood(ctx: InferenceContext, *, parameter_indices: Sequence[int], grid_size: int = 5) -> dict:
out = Path(ctx.output_dir) / "profiles"
plot_dir = Path(ctx.output_dir) / "plots" / "profile_likelihood"
@@ -583,7 +584,87 @@ def _plot_profile(df: pd.DataFrame, path: Path) -> None:
fig.savefig(path, dpi=300)
plt.close(fig)
+def _np_softplus(x: np.ndarray) -> np.ndarray:
+ """Stable NumPy softplus: log(1 + exp(x))."""
+ x = np.asarray(x, dtype=np.float64)
+ return np.log1p(np.exp(-np.abs(x))) + np.maximum(x, 0.0)
+
+
+def _theta_to_unit_interval(theta, lower, upper, *, eps: float = 1e-6) -> np.ndarray:
+ """Map raw theta into unit interval for NumPyro initialization."""
+ theta = np.asarray(theta, dtype=np.float64)
+ lower = np.asarray(lower, dtype=np.float64)
+ upper = np.asarray(upper, dtype=np.float64)
+
+ width = upper - lower
+
+ if np.any(~np.isfinite(lower)) or np.any(~np.isfinite(upper)):
+ bad = np.where(~np.isfinite(lower) | ~np.isfinite(upper))[0].tolist()
+ raise ValueError(
+ f"Posterior raw bounds contain non-finite values at indices: {bad}. "
+ "Check inv_softplus(0.0); use a small positive lower bound such as 1e-8."
+ )
+
+ if np.any(width <= 0):
+ bad = np.where(width <= 0)[0].tolist()
+ raise ValueError(f"Posterior raw bounds have upper <= lower at indices: {bad}")
+
+ unit = (theta - lower) / width
+ return np.clip(unit, eps, 1.0 - eps)
+
+
+def _is_softplus_raw_parameter(name: str) -> bool:
+ """Parameters stored in theta as raw softplus coordinates."""
+ name = str(name)
+
+ return (
+ name.startswith("c_k[")
+ or name.startswith("A_i[")
+ or name.startswith("B_i[")
+ or name.startswith("C_i[")
+ or name.startswith("D_i[")
+ or name.startswith("Dp_i[")
+ or name.startswith("E_i[")
+ or name == "tf_scale"
+ )
+
+def _raw_theta_samples_to_physical_df(
+ theta_raw_samples: np.ndarray,
+ names: Sequence[str],
+) -> pd.DataFrame:
+ """Convert raw posterior theta samples to physical parameter samples."""
+ theta_raw_samples = np.asarray(theta_raw_samples, dtype=np.float64)
+
+ out = {}
+
+ for i, name in enumerate(names):
+ if _is_softplus_raw_parameter(name):
+ out[name] = _np_softplus(theta_raw_samples[:, i])
+ else:
+ out[name] = theta_raw_samples[:, i]
+
+ return pd.DataFrame(out)
+
+
+def _assert_physical_samples_valid(sample_df: pd.DataFrame, names: Sequence[str]) -> None:
+ """Check that softplus-constrained parameters are non-negative after transform."""
+ cols = [
+ name for name in names
+ if name in sample_df.columns and _is_softplus_raw_parameter(name)
+ ]
+
+ if not cols:
+ return
+
+ mins = sample_df[cols].min(axis=0)
+ bad = mins[mins < -1e-12]
+
+ if not bad.empty:
+ raise RuntimeError(
+ "Physical posterior samples contain negative values after softplus "
+ f"conversion: {bad.to_dict()}"
+ )
def run_numpyro_posterior(
ctx: InferenceContext,
*,
@@ -601,57 +682,101 @@ def run_numpyro_posterior(
from numpyro.infer import MCMC, NUTS
except ImportError as exc:
raise RuntimeError(
- "NumPyro posterior inference requires the optional 'numpyro' dependency. Install numpyro to run posterior analysis.") from exc
+ "NumPyro posterior inference requires the optional 'numpyro' dependency. "
+ "Install numpyro to run posterior analysis."
+ ) from exc
+
out = Path(ctx.output_dir) / "posterior"
plot_dir = Path(ctx.output_dir) / "plots" / "posterior"
out.mkdir(parents=True, exist_ok=True)
plot_dir.mkdir(parents=True, exist_ok=True)
- lower_np = np.asarray(ctx.lower, dtype=np.float64).copy()
- upper_np = np.asarray(ctx.upper, dtype=np.float64).copy()
- bad = upper_np <= lower_np
- if np.any(bad):
- upper_np[bad] = lower_np[bad] + 1e-12
+ names = _param_names(len(ctx.theta0), ctx.parameter_names)
+
+ lower_raw_np = np.asarray(ctx.lower, dtype=np.float64).copy()
+ upper_raw_np = np.asarray(ctx.upper, dtype=np.float64).copy()
+ theta0_raw_np = np.asarray(ctx.theta0, dtype=np.float64).copy()
+
+ width_raw_np = upper_raw_np - lower_raw_np
+
+ if np.any(~np.isfinite(lower_raw_np)) or np.any(~np.isfinite(upper_raw_np)):
+ bad = np.where(~np.isfinite(lower_raw_np) | ~np.isfinite(upper_raw_np))[0]
+ bad_names = [names[i] for i in bad]
+ raise ValueError(
+ "Posterior raw bounds contain non-finite values for parameters: "
+ f"{bad_names}. "
+ "Likely cause: inv_softplus(0.0). Use lower bound 1e-8 instead of 0.0."
+ )
+
+ if np.any(width_raw_np <= 0):
+ bad = np.where(width_raw_np <= 0)[0]
+ bad_names = [names[i] for i in bad]
+ raise ValueError(
+ f"Posterior raw bounds have upper <= lower for parameters: {bad_names}"
+ )
- theta_center_np = np.clip(
- np.asarray(ctx.theta0, dtype=np.float64),
- lower_np + 1e-8,
- upper_np - 1e-8,
+ theta_unit_center_np = _theta_to_unit_interval(
+ theta0_raw_np,
+ lower_raw_np,
+ upper_raw_np,
)
- theta_center = jnp.asarray(theta_center_np, dtype=jnp.float64)
- lower = jnp.asarray(lower_np, dtype=jnp.float64)
- upper = jnp.asarray(upper_np, dtype=jnp.float64)
+ lower_raw = jnp.asarray(lower_raw_np, dtype=jnp.float64)
+ upper_raw = jnp.asarray(upper_raw_np, dtype=jnp.float64)
+ width_raw = upper_raw - lower_raw
+
+ theta_unit_center = jnp.asarray(theta_unit_center_np, dtype=jnp.float64)
+
+ if ctx.fixed_mask is not None:
+ fixed_mask_np = np.asarray(ctx.fixed_mask, dtype=bool)
+ else:
+ fixed_mask_np = None
+
+ if ctx.fixed_values is not None:
+ fixed_values_raw_np = np.clip(
+ np.asarray(ctx.fixed_values, dtype=np.float64),
+ lower_raw_np,
+ upper_raw_np,
+ )
+ else:
+ fixed_values_raw_np = None
def model():
- theta = numpyro.sample(
- "theta",
- dist.Uniform(lower, upper).to_event(1),
+ # Sample in unit box, then map into valid raw softplus-theta bounds.
+ theta_unit = numpyro.sample(
+ "theta_unit",
+ dist.Uniform(
+ jnp.zeros_like(lower_raw),
+ jnp.ones_like(upper_raw),
+ ).to_event(1),
)
- if ctx.fixed_mask is not None and ctx.fixed_values is not None:
- fixed_mask = jnp.asarray(ctx.fixed_mask, dtype=bool)
- fixed_values = jnp.clip(
- jnp.asarray(ctx.fixed_values, dtype=jnp.float64),
- lower,
- upper,
- )
- theta = jnp.where(fixed_mask, fixed_values, theta)
+ theta_raw = lower_raw + width_raw * theta_unit
+
+ if fixed_mask_np is not None and fixed_values_raw_np is not None:
+ fixed_mask = jnp.asarray(fixed_mask_np, dtype=bool)
+ fixed_values_raw = jnp.asarray(fixed_values_raw_np, dtype=jnp.float64)
+ theta_raw = jnp.where(fixed_mask, fixed_values_raw, theta_raw)
sigma = numpyro.sample("sigma", dist.Exponential(1.0))
- objective = ctx.objective_fun(theta)
+ # Important:
+ # objective_fun expects raw softplus theta, not physical theta.
+ objective = ctx.objective_fun(theta_raw)
+
+ numpyro.deterministic("theta_raw", theta_raw)
numpyro.deterministic("scalar_objective", objective)
- numpyro.factor("objective_likelihood", -0.5 * objective / (sigma * sigma + 1e-6))
+
+ numpyro.factor(
+ "objective_likelihood",
+ -0.5 * objective / (sigma * sigma + 1e-6),
+ )
kernel = NUTS(
model,
init_strategy=numpyro.infer.init_to_value(
- values=
- {
- "theta": theta_center
- }
- )
+ values={"theta_unit": theta_unit_center}
+ ),
)
mcmc = MCMC(
@@ -672,37 +797,128 @@ def model():
arr = np.asarray(value)
if arr.ndim <= 1:
diagnostics[key] = arr.tolist()
+
with open(out / "posterior_extra_fields.json", "w") as f:
json.dump(diagnostics, f, indent=2)
samples = mcmc.get_samples(group_by_chain=False)
- names = _param_names(len(ctx.theta0), ctx.parameter_names)
- theta_samples = np.asarray(samples["theta"])
- sample_df = pd.DataFrame(theta_samples, columns=names)
- sample_df["sigma"] = np.asarray(samples["sigma"])
+ if "theta_raw" in samples:
+ theta_raw_samples = np.asarray(samples["theta_raw"], dtype=np.float64)
+ else:
+ theta_unit_samples = np.asarray(samples["theta_unit"], dtype=np.float64)
+ theta_raw_samples = lower_raw_np + width_raw_np * theta_unit_samples
+
+ if fixed_mask_np is not None and fixed_values_raw_np is not None:
+ theta_raw_samples[:, fixed_mask_np] = fixed_values_raw_np[fixed_mask_np]
+
+ # Safety check: raw samples must stay inside raw bounds.
+ theta_raw_min = theta_raw_samples.min(axis=0)
+ theta_raw_max = theta_raw_samples.max(axis=0)
+
+ below = theta_raw_min < (lower_raw_np - 1e-10)
+ above = theta_raw_max > (upper_raw_np + 1e-10)
+
+ if np.any(below) or np.any(above):
+ bad = np.where(below | above)[0]
+ bad_names = [names[i] for i in bad]
+ raise RuntimeError(
+ "Raw posterior samples escaped saved raw bounds for parameters: "
+ f"{bad_names}"
+ )
+
+ raw_sample_df = pd.DataFrame(theta_raw_samples, columns=names)
+ raw_sample_df["sigma"] = np.asarray(samples["sigma"], dtype=np.float64)
+
+ if "scalar_objective" in samples:
+ raw_sample_df["scalar_objective"] = np.asarray(
+ samples["scalar_objective"],
+ dtype=np.float64,
+ )
+
+ raw_sample_df["data_mode"] = ctx.mode.data_mode
+ raw_sample_df.to_csv(out / "posterior_samples_raw.csv", index=False)
+
+ # Main posterior output: physical biological parameter scale.
+ sample_df = _raw_theta_samples_to_physical_df(theta_raw_samples, names)
+
+ sample_df["sigma"] = np.asarray(samples["sigma"], dtype=np.float64)
if "scalar_objective" in samples:
- sample_df["scalar_objective"] = np.asarray(samples["scalar_objective"])
+ sample_df["scalar_objective"] = np.asarray(
+ samples["scalar_objective"],
+ dtype=np.float64,
+ )
+
sample_df["data_mode"] = ctx.mode.data_mode
+
+ _assert_physical_samples_valid(sample_df, names)
+
sample_df.to_csv(out / "posterior_samples.csv", index=False)
+
summary_rows = []
for col in names + ["sigma"]:
vals = sample_df[col].to_numpy(dtype=np.float64)
- summary_rows.append({"parameter": col, "mean": vals.mean(), "median": np.median(vals), "sd": vals.std(ddof=0),
- "ci_05": np.quantile(vals, 0.05), "ci_95": np.quantile(vals, 0.95),
- "ess": float(len(vals)), "r_hat": np.nan, "data_mode": ctx.mode.data_mode})
+
+ summary_rows.append(
+ {
+ "parameter": col,
+ "mean": float(vals.mean()),
+ "median": float(np.median(vals)),
+ "sd": float(vals.std(ddof=0)),
+ "ci_05": float(np.quantile(vals, 0.05)),
+ "ci_95": float(np.quantile(vals, 0.95)),
+ "ess": float(len(vals)),
+ "r_hat": np.nan,
+ "data_mode": ctx.mode.data_mode,
+ "scale": "physical",
+ }
+ )
summary = pd.DataFrame(summary_rows)
summary.to_csv(out / "posterior_summary.csv", index=False)
- predictive = sample_df[
- ["scalar_objective", "data_mode"]].copy() if "scalar_objective" in sample_df else pd.DataFrame(
- {"data_mode": [ctx.mode.data_mode]})
+
+ raw_summary_rows = []
+
+ for col in names + ["sigma"]:
+ vals = raw_sample_df[col].to_numpy(dtype=np.float64)
+
+ raw_summary_rows.append(
+ {
+ "parameter": col,
+ "mean": float(vals.mean()),
+ "median": float(np.median(vals)),
+ "sd": float(vals.std(ddof=0)),
+ "ci_05": float(np.quantile(vals, 0.05)),
+ "ci_95": float(np.quantile(vals, 0.95)),
+ "ess": float(len(vals)),
+ "r_hat": np.nan,
+ "data_mode": ctx.mode.data_mode,
+ "scale": "raw_softplus",
+ }
+ )
+
+ raw_summary = pd.DataFrame(raw_summary_rows)
+ raw_summary.to_csv(out / "posterior_summary_raw.csv", index=False)
+
+ if "scalar_objective" in sample_df.columns:
+ predictive = sample_df[["scalar_objective", "data_mode"]].copy()
+ else:
+ predictive = pd.DataFrame({"data_mode": [ctx.mode.data_mode]})
+
predictive.to_csv(out / "posterior_predictive.csv", index=False)
+
_plot_posterior(sample_df, summary, plot_dir)
- return {"samples": sample_df, "summary": summary, "posterior_predictive": predictive, "output_dir": out}
+ return {
+ "samples": sample_df,
+ "raw_samples": raw_sample_df,
+ "summary": summary,
+ "raw_summary": raw_summary,
+ "posterior_predictive": predictive,
+ "output_dir": out,
+ }
def _run_single_numpyro_chain_process(
ctx: InferenceContext,
@@ -966,18 +1182,215 @@ def run_numpyro_posterior_standalone_processes(
"output_dir": out,
}
+def _safe_plot_name(name: str, max_len: int = 140) -> str:
+ """Make parameter names safe for filenames."""
+ safe = (
+ str(name)
+ .replace("/", "_")
+ .replace("\\", "_")
+ .replace("[", "_")
+ .replace("]", "_")
+ .replace("(", "_")
+ .replace(")", "_")
+ .replace(",", "_")
+ .replace(" ", "_")
+ .replace(":", "_")
+ .replace(";", "_")
+ )
+ return safe[:max_len]
-def _plot_posterior(samples: pd.DataFrame, summary: pd.DataFrame, plot_dir: Path) -> None:
- numeric = [
- c for c in samples.columns
- if pd.api.types.is_numeric_dtype(samples[c])
+
+def _smooth_density_1d(values: np.ndarray, grid_size: int = 256) -> tuple[np.ndarray, np.ndarray]:
+ """Small NumPy-only Gaussian KDE.
+
+ Avoids scipy/seaborn dependency.
+ Returns x_grid, density.
+ """
+ values = np.asarray(values, dtype=np.float64)
+ values = values[np.isfinite(values)]
+
+ if values.size == 0:
+ return np.array([0.0, 1.0]), np.array([0.0, 0.0])
+
+ if values.size == 1 or np.allclose(values, values[0]):
+ center = float(values[0])
+ span = max(abs(center) * 0.05, 1e-3)
+ x_grid = np.linspace(center - span, center + span, grid_size)
+ density = np.exp(-0.5 * ((x_grid - center) / (span / 4.0)) ** 2)
+ density = density / max(float(density.max()), 1e-12)
+ return x_grid, density
+
+ vmin = float(np.min(values))
+ vmax = float(np.max(values))
+ span = vmax - vmin
+
+ pad = 0.1 * span
+ x_grid = np.linspace(vmin - pad, vmax + pad, grid_size)
+
+ sd = float(np.std(values, ddof=1))
+ n = values.size
+
+ # Silverman's rule. Safe fallback for near-zero variance.
+ bandwidth = 1.06 * sd * (n ** (-1.0 / 5.0))
+ bandwidth = max(bandwidth, span / 200.0, 1e-8)
+
+ z = (x_grid[:, None] - values[None, :]) / bandwidth
+ density = np.exp(-0.5 * z * z).mean(axis=1)
+ density = density / (bandwidth * np.sqrt(2.0 * np.pi))
+
+ max_density = float(np.max(density))
+ if max_density > 0:
+ density = density / max_density
+
+ return x_grid, density
+
+
+def _plot_ridgeline_parameter_distributions(
+ samples: pd.DataFrame,
+ summary: pd.DataFrame,
+ plot_dir: Path,
+ *,
+ max_params_per_fig: int = 25,
+ include_sigma: bool = False,
+) -> None:
+ """Plot elegant batched ridgeline posterior distributions.
+
+ Each parameter is normalized to its own posterior range on the x-axis.
+ This makes the plot readable even when parameters have very different scales.
+ Individual density plots still preserve the real parameter scale.
+ """
+ ridge_dir = plot_dir / "ridge"
+ ridge_dir.mkdir(parents=True, exist_ok=True)
+
+ if summary.empty or "parameter" not in summary.columns:
+ logger.warning("[Posterior] Empty summary; skipping ridgeline posterior plot.")
+ return
+
+ params = [
+ str(p)
+ for p in summary["parameter"].astype(str).tolist()
+ if str(p) in samples.columns
+ and pd.api.types.is_numeric_dtype(samples[str(p)])
]
+ if not include_sigma:
+ params = [p for p in params if p != "sigma"]
+
+ if not params:
+ logger.warning("[Posterior] No numeric parameters available for ridgeline plot.")
+ return
+
+ batches = [
+ params[i:i + max_params_per_fig]
+ for i in range(0, len(params), max_params_per_fig)
+ ]
+
+ for batch_id, batch in enumerate(batches):
+ fig_height = max(6.0, 0.38 * len(batch) + 1.5)
+ fig, ax = plt.subplots(figsize=(11, fig_height))
+
+ y_positions = np.arange(len(batch))[::-1]
+
+ for y, param in zip(y_positions, batch):
+ values = samples[param].to_numpy(dtype=np.float64)
+ values = values[np.isfinite(values)]
+
+ if values.size == 0:
+ continue
+
+ vmin = float(np.min(values))
+ vmax = float(np.max(values))
+
+ if np.isclose(vmin, vmax):
+ x_norm = np.linspace(0.45, 0.55, 256)
+ density = np.exp(-0.5 * ((x_norm - 0.5) / 0.015) ** 2)
+ density = density / max(float(density.max()), 1e-12)
+ else:
+ x_grid, density = _smooth_density_1d(values)
+ x_norm = (x_grid - vmin) / (vmax - vmin)
+ x_norm = np.clip(x_norm, 0.0, 1.0)
+
+ ridge_height = 0.75
+ y_curve = y + ridge_height * density
+
+ ax.fill_between(
+ x_norm,
+ y,
+ y_curve,
+ alpha=0.55,
+ linewidth=0.0,
+ )
+
+ ax.plot(
+ x_norm,
+ y_curve,
+ linewidth=1.0,
+ )
+
+ q05, q50, q95 = np.quantile(values, [0.05, 0.5, 0.95])
+
+ if not np.isclose(vmin, vmax):
+ q05n = (q05 - vmin) / (vmax - vmin)
+ q50n = (q50 - vmin) / (vmax - vmin)
+ q95n = (q95 - vmin) / (vmax - vmin)
+
+ ax.plot(
+ [q05n, q95n],
+ [y + 0.05, y + 0.05],
+ linewidth=2.0,
+ )
+
+ ax.scatter(
+ [q50n],
+ [y + 0.05],
+ s=18,
+ zorder=3,
+ )
+
+ ax.text(
+ 1.03,
+ y + 0.05,
+ f"median={q50:.3g}",
+ va="center",
+ ha="left",
+ fontsize=8,
+ )
+
+ ax.set_yticks(y_positions)
+ ax.set_yticklabels(batch, fontsize=8)
+
+ ax.set_xlim(0.0, 1.22)
+ ax.set_xlabel("Normalized posterior range per parameter")
+ ax.set_title("Posterior parameter distributions")
+ ax.set_frame_on(False)
+
+ ax.tick_params(axis="y", length=0)
+ ax.grid(axis="x", alpha=0.25)
+
+ fig.tight_layout()
+ fig.savefig(
+ ridge_dir / f"posterior_ridge_{batch_id:03d}.png",
+ dpi=300,
+ bbox_inches="tight",
+ )
+ plt.close(fig)
+
+def _plot_posterior(samples: pd.DataFrame, summary: pd.DataFrame, plot_dir: Path) -> None:
density_dir = plot_dir / "density"
density_dir.mkdir(parents=True, exist_ok=True)
- for col in numeric:
- values = samples[col].to_numpy()
+ if summary.empty or "parameter" not in summary.columns:
+ logger.warning("[Posterior] Empty posterior summary; skipping posterior plots.")
+ return
+
+ plot_cols = [
+ str(p) for p in summary["parameter"].astype(str).tolist()
+ if str(p) in samples.columns
+ and pd.api.types.is_numeric_dtype(samples[str(p)])
+ ]
+
+ for col in plot_cols:
+ values = samples[col].to_numpy(dtype=np.float64)
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(values)
@@ -997,21 +1410,33 @@ def _plot_posterior(samples: pd.DataFrame, summary: pd.DataFrame, plot_dir: Path
fig.savefig(density_dir / f"density_{col}.png", dpi=300)
plt.close(fig)
- n_params = len(summary)
+ summary_plot = summary[
+ summary["parameter"].astype(str).isin(plot_cols)
+ ].copy()
+
+ if summary_plot.empty:
+ logger.warning("[Posterior] No numeric posterior parameters available for interval plot.")
+ return
+
+ n_params = len(summary_plot)
fig_width = max(10, 0.75 * n_params)
fig_height = 5
fig, ax = plt.subplots(figsize=(fig_width, fig_height))
- x = range(n_params)
+ x = np.arange(n_params)
+
+ median = summary_plot["median"].to_numpy(dtype=np.float64)
+ ci_05 = summary_plot["ci_05"].to_numpy(dtype=np.float64)
+ ci_95 = summary_plot["ci_95"].to_numpy(dtype=np.float64)
ax.errorbar(
x,
- summary["median"],
+ median,
yerr=[
- summary["median"] - summary["ci_05"],
- summary["ci_95"] - summary["median"],
+ median - ci_05,
+ ci_95 - median,
],
fmt="o",
capsize=4,
@@ -1019,7 +1444,7 @@ def _plot_posterior(samples: pd.DataFrame, summary: pd.DataFrame, plot_dir: Path
ax.set_xticks(list(x))
ax.set_xticklabels(
- summary["parameter"].astype(str),
+ summary_plot["parameter"].astype(str),
rotation=60,
ha="right",
rotation_mode="anchor",
@@ -1039,3 +1464,11 @@ def _plot_posterior(samples: pd.DataFrame, summary: pd.DataFrame, plot_dir: Path
fig.savefig(plot_dir / "credible_intervals.png", dpi=300)
plt.close(fig)
+
+ _plot_ridgeline_parameter_distributions(
+ samples=samples,
+ summary=summary,
+ plot_dir=plot_dir,
+ max_params_per_fig=25,
+ include_sigma=False,
+ )
\ No newline at end of file
diff --git a/networkmodel/OptimizationProblem.py b/networkmodel/OptimizationProblem.py
index 01d690a..4f6d2ae 100644
--- a/networkmodel/OptimizationProblem.py
+++ b/networkmodel/OptimizationProblem.py
@@ -18,6 +18,7 @@
logger = setup_logger(log_dir=RESULTS_DIR)
+
def _validate_slice_layout_covers_bounds(slices, bounds_size: int) -> int:
"""Validate that non-empty theta slices cover [0, bounds_size) exactly once."""
bounds_size = int(bounds_size)
@@ -87,6 +88,7 @@ def _validate_slice_layout_covers_bounds(slices, bounds_size: int) -> int:
return bounds_size
+
def build_weight_functions(method_protein="uniform", method_rna="uniform", time_grid=None):
"""Build placeholder weight functions for scalar optimization
diff --git a/networkmodel/PosteriorObjective.py b/networkmodel/PosteriorObjective.py
index de561ac..f7f0c2d 100644
--- a/networkmodel/PosteriorObjective.py
+++ b/networkmodel/PosteriorObjective.py
@@ -11,7 +11,7 @@
from pathlib import Path
from types import SimpleNamespace
-from config.constants import RESULTS_DIR
+from networkmodel.config import RESULTS_DIR
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
diff --git a/networkmodel/ProfileWorker.py b/networkmodel/ProfileWorker.py
index 4faff62..feedd55 100644
--- a/networkmodel/ProfileWorker.py
+++ b/networkmodel/ProfileWorker.py
@@ -57,10 +57,10 @@ def _cleanup_after_grid_step(*objs) -> None:
def run_one_parameter_profile(
- *,
- run_config_path: str | Path,
- parameter_index: int,
- grid_size: int,
+ *,
+ run_config_path: str | Path,
+ parameter_index: int,
+ grid_size: int,
) -> dict:
run_config_path = Path(run_config_path)
@@ -213,7 +213,7 @@ def run_one_parameter_profile(
if not finite.empty:
finite_min = finite["objective_value"].min()
partial_df["delta_objective"] = (
- partial_df["objective_value"] - finite_min
+ partial_df["objective_value"] - finite_min
)
else:
partial_df["delta_objective"] = np.inf
@@ -282,4 +282,4 @@ def main() -> None:
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/networkmodel/backend.py b/networkmodel/backend.py
index bc9ba5b..1daf266 100644
--- a/networkmodel/backend.py
+++ b/networkmodel/backend.py
@@ -254,12 +254,6 @@ def make_networkmodel_rhs(sys, slices=None):
N = int(idx.N)
max_sites = int(np.max(idx.n_sites)) if idx.N else 0
max_states = int(np.max(getattr(idx, "n_states", np.ones(idx.N, dtype=np.int32)))) if idx.N else 1
- trans_from = jnp.asarray(getattr(sys, "trans_from", np.zeros(0, dtype=np.int32)), dtype=jnp.int32)
- trans_to = jnp.asarray(getattr(sys, "trans_to", np.zeros(0, dtype=np.int32)), dtype=jnp.int32)
- trans_site = jnp.asarray(getattr(sys, "trans_site", np.zeros(0, dtype=np.int32)), dtype=jnp.int32)
- trans_off = jnp.asarray(getattr(sys, "trans_off", np.zeros(N, dtype=np.int32)), dtype=jnp.int32)
- trans_n = jnp.asarray(getattr(sys, "trans_n", np.zeros(N, dtype=np.int32)), dtype=jnp.int32)
- max_trans = int(np.max(getattr(sys, "trans_n", np.zeros(N, dtype=np.int32)))) if N else 0
def _params(args):
if isinstance(args, dict):
@@ -279,6 +273,17 @@ def _synth(Ai, tf_scale, u_raw):
Ai / (1.0 + tf_scale * jnp.abs(u)),
)
+ # Static metadata for compact MODEL == 2 JAX tracing.
+ # Do not use global max_states/max_sites loops inside the jitted RHS.
+ offsets_list = [int(x) for x in np.asarray(idx.offset_y)]
+ site_offsets_list = [int(x) for x in np.asarray(idx.offset_s)]
+ n_sites_list = [int(x) for x in np.asarray(idx.n_sites)]
+ n_states_list = [
+ int(x) for x in np.asarray(
+ getattr(idx, "n_states", np.ones(idx.N, dtype=np.int32))
+ )
+ ]
+
def rhs(t, y, args):
par = _params(args)
@@ -296,11 +301,9 @@ def _stepwise(row):
off = offsets[i]
drv = driver_map[i]
if model_id == 2:
- ar = jnp.arange(max_states, dtype=jnp.int32)
- valid = ar < n_states[i]
- pos = jnp.minimum(off + 1 + ar, y.shape[0] - 1)
- vals = jnp.where(valid, y[pos], 0.0)
- total_p = jnp.sum(vals)
+ p0 = offsets_list[i] + 1
+ nst_i = n_states_list[i]
+ total_p = jnp.sum(y[p0:p0 + nst_i])
else:
ar = jnp.arange(max_sites, dtype=jnp.int32)
valid = ar < n_sites[i]
@@ -319,50 +322,99 @@ def _stepwise(row):
R = y[off]
synth = _synth(par["A_i"][i], par["tf_scale"], TF_inputs[i])
dy = dy.at[off].set(synth - par["B_i"][i] * R)
+ # if model_id == 2:
+ # # Combinatorial model: mirror models.combinatorial_rhs for the
+ # # Diffrax/JAX path. Translation feeds only mask 0, while explicit
+ # # phosphorylation transitions and implicit bit dephosphorylation
+ # # transitions move mass among all 2^n mask states.
+ # p0 = off + 1
+ # nst = n_states[i]
+ # if max_states > 0:
+ # P0 = y[p0]
+ # dy = dy.at[p0].add(par["C_i"][i] * R - par["D_i"][i] * P0)
+ #
+ # # Dephosphorylation and per-site phospho-state decay for masks m > 0.
+ # for m in range(1, max_states):
+ # valid_m = m < nst
+ # pos_m = jnp.minimum(p0 + m, y.shape[0] - 1)
+ # Pm = jnp.where(valid_m, y[pos_m], 0.0)
+ # dp_rate = 0.0
+ # for j in range(max_sites):
+ # bit_set = ((m >> j) & 1) != 0
+ # valid_bit = valid_m & (j < ns) & bit_set
+ # flat_j = jnp.minimum(s_off + j, par["Dp_i"].shape[0] - 1)
+ # to = m ^ (1 << j)
+ # pos_to = jnp.minimum(p0 + to, y.shape[0] - 1)
+ # flux = jnp.where(valid_bit, par["E_i"][i] * Pm, 0.0)
+ # dy = dy.at[pos_m].add(-flux)
+ # dy = dy.at[pos_to].add(flux)
+ # dp_rate = dp_rate + jnp.where(valid_bit, par["Dp_i"][flat_j] + par["D_i"][i], 0.0)
+ # dy = dy.at[pos_m].add(-dp_rate * Pm)
+ #
+ # # Explicit phosphorylation transitions, generated per protein
+ # # in the same mask/site order as the historical dense arrays.
+ # for m in range(max_states):
+ # valid_m = m < nst
+ # for j in range(max_sites):
+ # bit_unset = ((m >> j) & 1) == 0
+ # valid_tr = valid_m & (j < ns) & bit_unset
+ # to = m | (1 << j)
+ # pos_frm = jnp.minimum(p0 + m, y.shape[0] - 1)
+ # pos_to = jnp.minimum(p0 + to, y.shape[0] - 1)
+ # flat_j = jnp.minimum(s_off + j, S_all.shape[0] - 1)
+ # flux = jnp.where(valid_tr, S_all[flat_j] * y[pos_frm], 0.0)
+ # dy = dy.at[pos_frm].add(-flux)
+ # dy = dy.at[pos_to].add(flux)
if model_id == 2:
- # Combinatorial model: mirror models.combinatorial_rhs for the
- # Diffrax/JAX path. Translation feeds only mask 0, while explicit
- # phosphorylation transitions and implicit bit dephosphorylation
- # transitions move mass among all 2^n mask states.
- p0 = off + 1
- nst = n_states[i]
- if max_states > 0:
- P0 = y[p0]
- dy = dy.at[p0].add(par["C_i"][i] * R - par["D_i"][i] * P0)
-
- # Dephosphorylation and per-site phospho-state decay for masks m > 0.
- for m in range(1, max_states):
- valid_m = m < nst
- pos_m = jnp.minimum(p0 + m, y.shape[0] - 1)
- Pm = jnp.where(valid_m, y[pos_m], 0.0)
- dp_rate = 0.0
- for j in range(max_sites):
- bit_set = ((m >> j) & 1) != 0
- valid_bit = valid_m & (j < ns) & bit_set
- flat_j = jnp.minimum(s_off + j, par["Dp_i"].shape[0] - 1)
- to = m ^ (1 << j)
- pos_to = jnp.minimum(p0 + to, y.shape[0] - 1)
- flux = jnp.where(valid_bit, par["E_i"][i] * Pm, 0.0)
- dy = dy.at[pos_m].add(-flux)
- dy = dy.at[pos_to].add(flux)
- dp_rate = dp_rate + jnp.where(valid_bit, par["Dp_i"][flat_j] + par["D_i"][i], 0.0)
- dy = dy.at[pos_m].add(-dp_rate * Pm)
-
- # Explicit phosphorylation transitions from the precomputed sparse
- # hypercube graph. Use S_all at the flattened site index so rates
- # remain differentiable through kinase scales and site parameters.
- for k in range(max_trans):
- tr_idx = jnp.minimum(trans_off[i] + k, trans_from.shape[0] - 1)
- valid_tr = k < trans_n[i]
- frm = trans_from[tr_idx]
- to = trans_to[tr_idx]
- j = trans_site[tr_idx]
- pos_frm = jnp.minimum(p0 + frm, y.shape[0] - 1)
- pos_to = jnp.minimum(p0 + to, y.shape[0] - 1)
- flat_j = jnp.minimum(s_off + j, S_all.shape[0] - 1)
- flux = jnp.where(valid_tr, S_all[flat_j] * y[pos_frm], 0.0)
- dy = dy.at[pos_frm].add(-flux)
- dy = dy.at[pos_to].add(flux)
+ # Vectorized combinatorial MODEL == 2 RHS.
+ # This avoids Python loops over max_states inside JAX tracing.
+ p0 = offsets_list[i] + 1
+ s_off_i = site_offsets_list[i]
+ ns_i = n_sites_list[i]
+ nst_i = n_states_list[i]
+
+ P = y[p0:p0 + nst_i]
+ states = jnp.arange(nst_i, dtype=jnp.int32)
+
+ dy_block = jnp.zeros_like(P)
+
+ # Translation feeds only mask 0.
+ dy_block = dy_block.at[0].add(
+ par["C_i"][i] * R - par["D_i"][i] * P[0]
+ )
+
+ # Site-wise vectorized transitions.
+ # Keep loop over sites only; do not loop over states in Python.
+ for j in range(ns_i):
+ bit = np.int32(1 << j)
+ flat_j = s_off_i + j
+
+ bit_set = (states & bit) != 0
+ bit_unset = ~bit_set
+
+ # Dephosphorylation transition:
+ # m -> m ^ bit for states where bit is set.
+ from_set = jnp.where(bit_set, P, 0.0)
+ to_clear = states ^ bit
+ flux_back = par["E_i"][i] * from_set
+
+ dy_block = dy_block - flux_back
+ dy_block = dy_block.at[to_clear].add(flux_back)
+
+ # Per-site phospho-state decay for states where bit is set.
+ decay = (par["Dp_i"][flat_j] + par["D_i"][i]) * from_set
+ dy_block = dy_block - decay
+
+ # Phosphorylation transition:
+ # m -> m | bit for states where bit is unset.
+ from_unset = jnp.where(bit_unset, P, 0.0)
+ to_set = states | bit
+ flux_fwd = S_all[flat_j] * from_unset
+
+ dy_block = dy_block - flux_fwd
+ dy_block = dy_block.at[to_set].add(flux_fwd)
+
+ dy = dy.at[p0:p0 + nst_i].add(dy_block)
else:
P = y[off + 1]
ar = jnp.arange(max_sites, dtype=jnp.int32)
@@ -386,7 +438,6 @@ def _stepwise(row):
return rhs
-
def solve_diffrax(y0, t_eval, params=None, rhs=None, config: DiffraxSolverConfig | None = None):
"""Solve an ODE trajectory with Diffrax
diff --git a/networkmodel/config.py b/networkmodel/config.py
index c8808e3..c6ec4e5 100644
--- a/networkmodel/config.py
+++ b/networkmodel/config.py
@@ -2,11 +2,16 @@
import os
from config_loader import load_config_toml
-# cfg: PhosKinConfig loaded from ./config.toml; no default because import requires the file.
-if os.path.exists("config.toml"):
- cfg = load_config_toml("config.toml")
+CONFIG_ENV_VAR = "PHOSKINTIME_NETWORKMODEL_CONFIG"
+CONFIG_PATH = os.environ.get(CONFIG_ENV_VAR, "config.toml")
+
+# cfg: PhosKinConfig loaded from the selected TOML. networkmodel.runner sets
+# PHOSKINTIME_NETWORKMODEL_CONFIG after parsing --conf and before importing
+# modules that rely on this compatibility constants surface.
+if os.path.exists(CONFIG_PATH):
+ cfg = load_config_toml(CONFIG_PATH)
else:
- raise FileNotFoundError("config.toml not found in current directory.")
+ raise FileNotFoundError(f"networkmodel config not found: {CONFIG_PATH}")
def _as_bool(x):
diff --git a/networkmodel/dashboard_bundle.py b/networkmodel/dashboard_bundle.py
index fd942af..83db31a 100644
--- a/networkmodel/dashboard_bundle.py
+++ b/networkmodel/dashboard_bundle.py
@@ -76,7 +76,9 @@ def save_dashboard_bundle(
"df_pho_obs": df_pho,
}
- bundle_path = out / "dashboard_bundle.pkl"
+ artifacts_dir = out / "artifacts"
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
+ bundle_path = artifacts_dir / "dashboard_bundle.pkl"
with bundle_path.open("wb") as f:
pickle.dump(bundle, f, protocol=pickle.HIGHEST_PROTOCOL)
@@ -92,6 +94,6 @@ def load_dashboard_bundle(output_dir: str | Path) -> dict:
Returns:
Computed result from this routine.
"""
- p = Path(output_dir) / "dashboard_bundle.pkl"
+ p = Path(output_dir) / "artifacts" / "dashboard_bundle.pkl"
with p.open("rb") as f:
return pickle.load(f)
diff --git a/networkmodel/export.py b/networkmodel/export.py
index 8c24474..d5b9eff 100644
--- a/networkmodel/export.py
+++ b/networkmodel/export.py
@@ -1052,9 +1052,11 @@ def export_S_rates(sys, idx, output_dir, filename="S_rates_picked.csv", long=Tru
# ---- compute S matrix: shape (total_sites, n_bins) ----
if MODEL == 2:
- # Ensure cache matches current optimized c_k
- build_S_cache_into(sys.S_cache, sys.W_indptr, sys.W_indices, sys.W_data, sys.kin_Kmat, sys.c_k)
- S_mat = np.asarray(sys.S_cache, dtype=np.float64)
+ # The combinatorial RHS may use a 1-D current-rate work buffer for
+ # memory safety, but S_rates_picked.csv preserves the historical dense
+ # site-by-time export shape. Build that dense matrix only for export.
+ S_mat = np.empty((int(sys.n_W_rows), int(sys.kin_Kmat.shape[1])), dtype=np.float64)
+ build_S_cache_into(S_mat, sys.W_indptr, sys.W_indices, sys.W_data, sys.kin_Kmat, sys.c_k)
times = np.asarray(sys.kin_grid, dtype=float)
else:
# Dense kinase signal scaled by c_k
diff --git a/networkmodel/io.py b/networkmodel/io.py
index d25c2fb..6a71221 100644
--- a/networkmodel/io.py
+++ b/networkmodel/io.py
@@ -9,6 +9,23 @@
logger = setup_logger(log_dir=RESULTS_DIR)
+_MISSING_INPUT_VALUES = {"", "none", "null", "na", "n/a", "nan", "-"}
+
+
+def _is_missing_input_path(path) -> bool:
+ """Return True when an optional input path is intentionally absent."""
+ if path is None:
+ return True
+ return str(path).strip().lower() in _MISSING_INPUT_VALUES
+
+
+def _empty_tidy_rna() -> pd.DataFrame:
+ """Return an empty RNA observation table with the downstream-required schema."""
+ return pd.DataFrame({
+ "protein": pd.Series(dtype="object"),
+ "time": pd.Series(dtype="float64"),
+ "fc": pd.Series(dtype="float64"),
+ })
def load_data(args):
"""Load configured networkmodel input tables
@@ -229,26 +246,60 @@ def load_data(args):
# =========================================================================
logger.info(f"[Data] Loading RNA: {args.rna}")
- # 1. Read and Clean Headers
- df_rna_raw = pd.read_csv(args.rna)
- df_rna_raw = _normcols(df_rna_raw)
-
- # 2. Identify the Gene/Protein ID column
- gcol = _find_col(df_rna_raw, ["geneid", "mrna", "gene"])
+ if _is_missing_input_path(getattr(args, "rna", None)):
+ logger.info("[Data] RNA input not provided; continuing with empty RNA modality.")
+ df_rna = _empty_tidy_rna()
- # 3. Rename it to 'protein' so the rest of the pipeline understands it
- df_rna_raw = df_rna_raw.rename(columns={gcol: "protein"})
-
- # 4. Process (Scale -> Map Time -> Melt)
- # The function returns the final tidy dataframe directly.
- df_rna = process_and_scale_raw_data(
- df_rna_raw,
- time_points=TIME_POINTS_RNA,
- id_cols=["protein"],
- scale_method=SCALING_METHOD
- )
+ else:
+ rna_path = str(args.rna).strip()
+
+ if not os.path.exists(rna_path):
+ raise FileNotFoundError(
+ f"RNA input path does not exist: {rna_path!r}. "
+ "Use an empty string, None, 'NA', or '-' only when RNA is intentionally absent."
+ )
+
+ # 1. Read and clean headers
+ df_rna_raw = pd.read_csv(rna_path)
+ df_rna_raw = _normcols(df_rna_raw)
+
+ # 2. Identify the Gene/Protein ID column
+ gcol = _find_col(df_rna_raw, ["geneid", "mrna", "gene", "protein"])
+
+ if gcol is None:
+ raise ValueError(
+ f"Could not find RNA gene identifier column in {rna_path!r}. "
+ f"Expected one of: geneid, mrna, gene, protein. "
+ f"Found columns: {list(df_rna_raw.columns)}"
+ )
+
+ # 3. Rename it to 'protein' so the rest of the pipeline understands it
+ df_rna_raw = df_rna_raw.rename(columns={gcol: "protein"})
+
+ # 4. Process: scale -> map time -> melt
+ df_rna = process_and_scale_raw_data(
+ df_rna_raw,
+ time_points=TIME_POINTS_RNA,
+ id_cols=["protein"],
+ scale_method=SCALING_METHOD
+ )
+
+ df_rna = _normcols(df_rna)
+
+ required_cols = {"protein", "time", "fc"}
+ missing_cols = required_cols - set(df_rna.columns)
+ if missing_cols:
+ raise ValueError(
+ f"Processed RNA table is missing required columns: {missing_cols}. "
+ f"Found columns: {list(df_rna.columns)}"
+ )
+
+ df_rna = df_rna[["protein", "time", "fc"]].copy()
+ df_rna["protein"] = df_rna["protein"].astype(str).str.strip().str.upper()
+ df_rna["time"] = pd.to_numeric(df_rna["time"], errors="coerce")
+ df_rna["fc"] = pd.to_numeric(df_rna["fc"], errors="coerce")
+ df_rna = df_rna.dropna(subset=["protein", "time", "fc"]).reset_index(drop=True)
- # df_rna is now ready. It has columns: ["protein", "time", "fc"]
logger.info(f"[Data] Loaded {len(df_rna)} RNA points.")
return df_kin_clean, df_tf_clean, df_prot, df_pho, df_rna, kin_beta_map, tf_beta_map
diff --git a/networkmodel/models.py b/networkmodel/models.py
index 8284048..97eaf42 100644
--- a/networkmodel/models.py
+++ b/networkmodel/models.py
@@ -316,10 +316,9 @@ def _bit_index_from_lsb(lsb):
def combinatorial_rhs(
y, dy,
A_i, B_i, C_i, D_i, Dp_i, E_i, tf_scale,
- TF_inputs, S_cache, jb,
+ TF_inputs, S_rates,
offset_y, offset_s,
- n_sites, n_states,
- trans_from, trans_to, trans_site, trans_off, trans_n
+ n_sites, n_states
):
"""Evaluate the combinatorial topology right-hand side
@@ -334,21 +333,13 @@ def combinatorial_rhs(
E_i: Input value used by this routine.
tf_scale: Input value used by this routine.
TF_inputs: Input value used by this routine.
- S_cache: Input value used by this routine.
- jb: Input value used by this routine.
+ S_rates: Current per-site kinase signal vector.
offset_y: Input value used by this routine.
offset_s: Input value used by this routine.
n_sites: Input value used by this routine.
n_states: Input value used by this routine.
- trans_from: Input value used by this routine.
- trans_to: Input value used by this routine.
- trans_site: Input value used by this routine.
- trans_off: Input value used by this routine.
- trans_n: Input value used by this routine.
"""
N = A_i.shape[0]
- jb_loc = jb # local binding helps Numba
-
for i in range(N):
y_start = offset_y[i]
s_start = offset_s[i]
@@ -421,59 +412,70 @@ def combinatorial_rhs(
dy[base + m] -= dp_rate * Pm
# --- Phosphorylation Loop (Forward Transitions) ---
- # Uses pre-calculated sparse graph structure
- off = trans_off[i]
- ntr = trans_n[i]
- for k in range(ntr):
- frm = trans_from[off + k]
- to = trans_to[off + k]
- j = trans_site[off + k]
+ # Enumerate the same hypercube edges in the same order as the former
+ # dense transition arrays, but do not materialize O(2^n_sites*n_sites)
+ # arrays.
+ for m in range(nstates):
+ for j in range(ns):
+ bit = 1 << j
+ if (m & bit) == 0:
+ to = m | bit
+ rate = S_rates[s_start + j]
+ flux = rate * y[base + m]
- # Rate depends on time bucket 'jb_loc'
- rate = S_cache[s_start + j, jb_loc]
- flux = rate * y[base + frm]
+ dy[base + m] -= flux
+ dy[base + to] += flux
- dy[base + frm] -= flux
- dy[base + to] += flux
+def iter_random_transitions_for_sites(n_sites):
+ """Yield combinatorial forward transitions for one protein lazily.
-def build_random_transitions(idx):
- """Build transition arrays for combinatorial topology
-
- Args:
- idx: Input value used by this routine.
-
- Returns:
- Computed result from this routine.
+ The order is identical to the historical dense implementation: state mask
+ first, then site index, yielding only unset-bit phosphorylation edges.
+ """
+ ns = int(n_sites)
+ if ns <= 0:
+ return
+ nstates = 1 << ns
+ for m in range(nstates):
+ for j in range(ns):
+ if (m & (1 << j)) == 0:
+ yield m, m | (1 << j), j
+
+
+def count_random_transitions_for_sites(n_sites):
+ """Return the number of combinatorial forward transitions for n sites."""
+ ns = int(n_sites)
+ return 0 if ns <= 0 else ns * (1 << (ns - 1))
+
+
+def build_random_transitions(idx, *, dense_threshold_sites=4):
+ """Build small dense transition arrays for compatibility.
+
+ Large proteins are represented by metadata only; callers that need all
+ transitions should use :func:`iter_random_transitions_for_sites` instead.
"""
trans_from = []
trans_to = []
trans_site = []
trans_off = np.zeros(idx.N, dtype=np.int32)
trans_n = np.zeros(idx.N, dtype=np.int32)
+ dense_available = np.zeros(idx.N, dtype=np.bool_)
cur = 0
for i in range(idx.N):
ns = int(idx.n_sites[i])
trans_off[i] = cur
-
- if ns == 0:
- trans_n[i] = 0
- continue
-
- nstates = 1 << ns
- for m in range(nstates):
- for j in range(ns):
- # If bit j is NOT set in m, we can transition to m | (1< COMBINATORIAL_MAX_STATES_PER_PROTEIN:
+ raise MemoryError(
+ f"Unsafe combinatorial MODEL=2 state space for protein {protein!r}: "
+ f"n_sites={ns} requires n_states=2^n_sites={nstates:,}. "
+ "MODEL=2 scales exponentially as 2^n_sites; reduce the number of sites, "
+ "use sequential/distributive models, or lower the workload."
+ )
+ n_states_list.append(nstates)
+ self.n_states = np.asarray(n_states_list, dtype=np.int64)
# Offsets map standard indices to the flattened y-vector
self.offset_y = np.zeros(self.N, dtype=np.int32)
@@ -126,6 +142,22 @@ def __init__(self,
self.state_dim = curr_y
self.total_sites = int(curr_s)
+ if MODEL == 2:
+ if self.state_dim > COMBINATORIAL_MAX_TOTAL_STATE_DIM:
+ raise MemoryError(
+ f"Unsafe combinatorial MODEL=2 total state dimension: {self.state_dim:,} state variables. "
+ "MODEL=2 scales as 2^n_sites per protein; reduce sites, use sequential/distributive "
+ "models, or lower workload."
+ )
+ transition_count = int(sum((int(ns) * (1 << (int(ns) - 1))) if int(ns) > 0 else 0 for ns in self.n_sites))
+ dense_transition_mb = transition_count * 3 * np.dtype(np.int32).itemsize / (1024 ** 2)
+ traj_mb = self.state_dim * len(TIME_POINTS_PROTEIN) * np.dtype(np.float64).itemsize / (1024 ** 2)
+ logger.warning(
+ "[Model] Combinatorial MODEL=2 diagnostics: proteins=%d n_sites=%s n_states=%s "
+ "total_state_dim=%d estimated_trajectory=%.2f MiB dense_transition_arrays=%.2f MiB. "
+ "MODEL=2 scales exponentially as 2^n_sites.",
+ self.N, self.n_sites.tolist(), self.n_states.tolist(), self.state_dim, traj_mb, dense_transition_mb
+ )
self.kinase_indices_in_P = [self.p2i[k] for k in self.kinases if k in self.p2i]
self.p2k = {k: i for i, k in enumerate(self.kinases)}
@@ -269,18 +301,22 @@ def __init__(self, idx, W_global, tf_mat, kin_input, defaults, tf_deg):
# MODEL == 2 specific setup
# ------------------------------------------------------------
if MODEL == 2:
- # reusable work buffers (NO allocs in RHS)
+ # reusable work buffers (NO allocs in RHS). S_cache is a one-column
+ # compatibility buffer populated from the current kinase signal, not
+ # a dense site-by-time cache.
self.P_vec_work = np.zeros(self.n_TF_rows, dtype=np.float64)
self.TF_in_work = np.zeros(self.n_TF_rows, dtype=np.float64)
- self.S_cache = np.zeros((self.n_W_rows, self.kin_Kmat.shape[1]), dtype=np.float64)
+ self.S_cache = np.zeros(self.n_W_rows, dtype=np.float64)
- # precomputed transition lists for the combinatorial hypercube graph
+ # Dense transitions are retained only for small compatibility tests;
+ # RHS evaluation streams transitions from n_sites/n_states metadata.
(
self.trans_from,
self.trans_to,
self.trans_site,
self.trans_off,
self.trans_n,
+ self.trans_dense_available,
) = build_random_transitions(idx)
def update(self, c_k, A_i, B_i, C_i, D_i, Dp_i, E_i, tf_scale):
@@ -418,22 +454,16 @@ def rhs(self, t, y):
elif MODEL == 2:
if self.S_cache is None:
- raise ValueError("MODEL==2: System.S_cache is None. simulate_diffrax must set it.")
-
- jb = int(np.searchsorted(self.kin_grid, t, side="right") - 1)
- if jb < 0:
- jb = 0
- elif jb >= self.kin_grid.size:
- jb = self.kin_grid.size - 1
+ raise ValueError("MODEL==2: System.S_cache is None.")
+ self.S_cache[:] = S_all
combinatorial_rhs(
y, dy,
self.A_i, self.B_i, self.C_i, self.D_i, self.Dp_i, self.E_i, self.tf_scale,
TF_inputs,
- self.S_cache, jb,
+ self.S_cache,
self.idx.offset_y, self.idx.offset_s,
- self.idx.n_sites, self.idx.n_states,
- self.trans_from, self.trans_to, self.trans_site, self.trans_off, self.trans_n
+ self.idx.n_sites, self.idx.n_states
)
return dy
diff --git a/networkmodel/runner.py b/networkmodel/runner.py
index 07d3077..1e056c9 100644
--- a/networkmodel/runner.py
+++ b/networkmodel/runner.py
@@ -23,56 +23,20 @@
import argparse
import atexit
+import importlib
import json
import logging
import pickle
+import sys
import multiprocessing as mp
+from dataclasses import asdict, is_dataclass
+from pathlib import Path
+from typing import Any
-import numpy as np
-import pandas as pd
-
-from networkmodel.dashboard_bundle import save_dashboard_bundle
-from networkmodel.scan import run_hyperparameter_scan
-from networkmodel.sensitivity import run_sensitivity_analysis
-from networkmodel.InitialConditions import _dump_y0
-from networkmodel.BuildMatrix import build_W_parallel, build_tf_matrix
-from networkmodel.cache import prepare_fast_loss_data
-from networkmodel.config import TIME_POINTS_PROTEIN, TIME_POINTS_RNA, RESULTS_DIR, MAX_ITERATIONS, \
- SEED, REGULARIZATION_LAMBDA, REGULARIZATION_RNA, REGULARIZATION_PHOSPHO, TIME_POINTS_PHOSPHO, \
- REGULARIZATION_PROTEIN, NORMALIZE_FC_STEADY, USE_INITIAL_CONDITION_FROM_DATA, KINASE_NET_FILE, TF_NET_FILE, \
- MS_DATA_FILE, RNA_DATA_FILE, PHOSPHO_DATA_FILE, KINOPT_RESULTS_FILE, TFOPT_RESULTS_FILE, \
- WEIGHTING_METHOD_PROTEIN, WEIGHTING_METHOD_RNA, APP_NAME, VERSION, PARENT_PACKAGE, CITATION, DOI, GITHUB_URL, \
- DOCS_URL, SENSITIVITY_METRIC, SENSITIVITY_ANALYSIS, AVAILABLE_MODELS, HYPERPARAM_SCAN, MODEL, CORES, \
- N_STARTS, PROFILE_LIKELIHOOD, PROFILE_INDICES, PROFILE_GRID_SIZE, POSTERIOR_SAMPLING, \
- POSTERIOR_NUM_WARMUP, POSTERIOR_NUM_SAMPLES
-from networkmodel.io import load_data
-from networkmodel.network import Index, KinaseInput, System
-from networkmodel.OptimizationProblem import GlobalODEScalarObjective, build_weight_functions
-from networkmodel.params import init_raw_params, unpack_params
-from networkmodel.simulate import simulate_and_measure
-from networkmodel.utils import normalize_fc_to_t0, _base_idx, calculate_bio_bounds, \
- get_optimized_sets
-from networkmodel.export import export_pareto_front_to_excel, plot_goodness_of_fit, \
- export_results, save_gene_timeseries_plots, \
- export_S_rates, plot_s_rates_report, export_kinase_activities, \
- export_param_correlations, export_residuals, export_parameter_distributions
-from networkmodel.SteadyStateAnalysis import simulate_until_steady, plot_steady_state_all
-from networkmodel.backend import warn_deprecated_backend_options, detect_data_mode, JaxoptResult
-from networkmodel.BayesianInference import (
- InferenceContext,
- run_multistart,
- run_profile_likelihood_standalone_processes,
- run_numpyro_posterior_standalone_processes,
- configure_jax_parallelism,
-)
-
-from networkmodel.PosteriorObjective import write_posterior_payload
-from networkmodel.mode_outputs import write_scalar_result_tables
-from common.frechet import frechet_distance
-from config_loader import load_config_toml
-from config.config import setup_logger
+DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.toml"
+NETWORKMODEL_CONFIG_ENV = "PHOSKINTIME_NETWORKMODEL_CONFIG"
-logger = setup_logger(log_dir=RESULTS_DIR)
+logger = logging.getLogger(__name__)
global problem
@@ -88,82 +52,331 @@ def _close_log_handlers():
pass
+def _jsonable_runtime(value: Any) -> Any:
+ """Convert runtime configuration values to JSON/YAML-safe primitives."""
+ if isinstance(value, Path):
+ return str(value)
+ if is_dataclass(value):
+ return _jsonable_runtime(asdict(value))
+ if isinstance(value, dict):
+ return {str(k): _jsonable_runtime(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple, set)):
+ return [_jsonable_runtime(v) for v in value]
+ if hasattr(value, "tolist"):
+ return value.tolist()
+ try:
+ json.dumps(value)
+ return value
+ except TypeError:
+ return str(value)
+
+
+def _as_bool(value: Any) -> bool:
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.strip().lower() in {"1", "true", "yes", "y", "on"}
+ return bool(value)
+
+
+def _default_config_path() -> Path:
+ return DEFAULT_CONFIG_PATH
+
+
+def parse_config_path(argv: list[str] | None = None) -> Path | None:
+ """Parse only --conf so selected config is known before defaults are resolved."""
+ pre_parser = argparse.ArgumentParser(add_help=False)
+ pre_parser.add_argument("--conf", default=None)
+ known, _ = pre_parser.parse_known_args(argv)
+ return Path(known.conf).expanduser() if known.conf else None
+
+
+def _resolve_config_path(conf_path: Path | None) -> Path:
+ return (conf_path or _default_config_path()).expanduser().resolve()
+
+
+def load_networkmodel_config(conf_path: Path | None):
+ """Load selected networkmodel config and expose it to networkmodel.config importers."""
+ resolved = _resolve_config_path(conf_path)
+ os.environ[NETWORKMODEL_CONFIG_ENV] = str(resolved)
+ if "networkmodel.config" in sys.modules:
+ importlib.reload(sys.modules["networkmodel.config"])
+ from config_loader import load_config_toml
+
+ return load_config_toml(resolved)
+
+
+def _model_code(model: Any) -> int:
+ if isinstance(model, int):
+ return model
+ name = str(model).strip().lower()
+ mapping = {"distributive": 0, "sequential": 1, "successive": 1, "combinatorial": 2, "saturating": 4,
+ "saturation": 4}
+ return mapping.get(name, 4)
+
+
+def _config_defaults(config: Any, resolved_config_path: Path, supplied_conf_path: Path | None) -> dict[str, Any]:
+ default_path = _default_config_path().resolve()
+ return {
+ "conf": str(supplied_conf_path) if supplied_conf_path is not None else None,
+ "resolved_config_path": str(resolved_config_path),
+ "config_source": "custom" if supplied_conf_path is not None and resolved_config_path != default_path else "default",
+ "kinase_net": config.kinase_net,
+ "tf_net": config.tf_net,
+ "ms": config.ms_data,
+ "rna": config.rna_data,
+ "phospho": config.phospho_data,
+ "kinopt": config.kinopt_results,
+ "tfopt": config.tfopt_results,
+ "output_dir": config.results_dir,
+ "cores": config.cores,
+ "n_gen": config.maximum_iterations,
+ "seed": config.seed,
+ "lambda_prior": config.regularization_lambda,
+ "lambda_protein": config.regularization_protein,
+ "lambda_rna": config.regularization_rna,
+ "lambda_phospho": config.regularization_phospho,
+ "normalize_fc_steady": _as_bool(config.normalize_fc_steady),
+ "use_initial_condition_from_data": _as_bool(config.use_initial_condition_from_data),
+ "scan": _as_bool(config.hyperparam_scan),
+ "sensitivity": _as_bool(config.sensitivity_analysis),
+ "solver": "jaxopt",
+ "app_name": getattr(config, "app_name", "Phoskintime-Global"),
+ "version": getattr(config, "version", "0.1.0"),
+ "parent_package": getattr(config, "parent_package", "phoskintime"),
+ "citation": getattr(config, "citation", ""),
+ "doi": getattr(config, "doi", ""),
+ "github_url": getattr(config, "github_url", ""),
+ "docs_url": getattr(config, "docs_url", ""),
+ "available_models": tuple(getattr(config, "available_models", ()) or ()),
+ "model": getattr(config, "model", "combinatorial"),
+ "model_code": _model_code(getattr(config, "model", "combinatorial")),
+ "time_points_protein": config.time_points_prot,
+ "time_points_rna": config.time_points_rna,
+ "time_points_phospho": config.time_points_phospho,
+ "weighting_method_protein": getattr(config, "weighting_method_protein", "uniform"),
+ "weighting_method_rna": getattr(config, "weighting_method_rna", "uniform"),
+ "sensitivity_metric": getattr(config, "sensitivity_metric", "total_signal"),
+ "n_starts": int(getattr(config, "n_starts", 1)),
+ "profile_likelihood": _as_bool(getattr(config, "profile_likelihood", False)),
+ "profile_indices": str(getattr(config, "profile_indices", "")),
+ "profile_grid_size": int(getattr(config, "profile_grid_size", 10)),
+ "posterior_sampling": _as_bool(getattr(config, "posterior_sampling", False)),
+ "posterior_num_warmup": int(getattr(config, "posterior_num_warmup", 20)),
+ "posterior_num_samples": int(getattr(config, "posterior_num_samples", 30)),
+ "raw_config": config,
+ }
+
+
+def build_parser(config_defaults: dict[str, Any]) -> argparse.ArgumentParser:
+ """Build the full parser after config-backed defaults are known."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--conf", default=config_defaults["conf"],
+ help="Path to TOML config used for networkmodel defaults.")
+ parser.add_argument("--kinase-net", default=config_defaults["kinase_net"])
+ parser.add_argument("--tf-net", default=config_defaults["tf_net"])
+ parser.add_argument("--ms", default=config_defaults["ms"])
+ parser.add_argument("--rna", default=config_defaults["rna"])
+ parser.add_argument("--phospho", default=config_defaults["phospho"])
+ parser.add_argument("--kinopt", default=config_defaults["kinopt"])
+ parser.add_argument("--tfopt", default=config_defaults["tfopt"])
+ parser.add_argument("--output-dir", "--outdir", dest="output_dir", default=config_defaults["output_dir"])
+ parser.add_argument("--cores", type=int, default=config_defaults["cores"])
+ parser.add_argument("--n-gen", type=int, default=config_defaults["n_gen"])
+ parser.add_argument("--seed", type=int, default=config_defaults["seed"])
+ parser.add_argument("--lambda-prior", type=float, default=config_defaults["lambda_prior"])
+ parser.add_argument("--lambda-protein", type=float, default=config_defaults["lambda_protein"])
+ parser.add_argument("--lambda-rna", type=float, default=config_defaults["lambda_rna"])
+ parser.add_argument("--lambda-phospho", type=float, default=config_defaults["lambda_phospho"])
+ parser.add_argument("--normalize-fc-steady", action="store_true", default=config_defaults["normalize_fc_steady"])
+ parser.add_argument("--use-initial-condition-from-data", action="store_true",
+ default=config_defaults["use_initial_condition_from_data"])
+ parser.add_argument("--scan", action="store_true",
+ help="Run a hyperparameter scan using Optuna to find the best regularization parameters.",
+ default=config_defaults["scan"])
+ parser.add_argument("--sensitivity", action="store_true", help="Run a sensitivity analysis after optimization.",
+ default=config_defaults["sensitivity"])
+ parser.add_argument("--solver", type=str, choices=["jaxopt", "pymoo", "optuna"], default=config_defaults["solver"],
+ help="Choice of optimization solver. Legacy pymoo/optuna values map to jaxopt.")
+ return parser
+
+
+def resolve_runtime_config(args: argparse.Namespace, config_defaults: dict[str, Any]) -> argparse.Namespace:
+ """Attach selected-config metadata and config-only defaults to parsed args."""
+ for key, value in config_defaults.items():
+ if key not in vars(args):
+ setattr(args, key, value)
+ return args
+
+
+def parse_runtime_args(argv: list[str] | None = None) -> tuple[argparse.Namespace, dict[str, Any]]:
+ """Parse --conf first, load that config, then parse all args with selected defaults."""
+ supplied_conf_path = parse_config_path(argv)
+ resolved_config_path = _resolve_config_path(supplied_conf_path)
+ config = load_networkmodel_config(supplied_conf_path)
+ defaults = _config_defaults(config, resolved_config_path, supplied_conf_path)
+ parser = build_parser(defaults)
+ args = parser.parse_args(argv)
+ return resolve_runtime_config(args, defaults), defaults
+
+
+def _runtime_metadata_extra(args: argparse.Namespace) -> dict[str, Any]:
+ return {
+ "supplied_config_path": args.conf,
+ "resolved_config_path": args.resolved_config_path,
+ "config_source": args.config_source,
+ "effective_inputs": {
+ "kinase_net": args.kinase_net,
+ "tf_net": args.tf_net,
+ "ms": args.ms,
+ "rna": args.rna,
+ "phospho": args.phospho,
+ "kinopt": args.kinopt,
+ "tfopt": args.tfopt,
+ },
+ "effective_settings": {
+ "output_dir": args.output_dir,
+ "cores": args.cores,
+ "n_gen": args.n_gen,
+ "seed": args.seed,
+ "solver": args.solver,
+ "lambda_prior": args.lambda_prior,
+ "lambda_protein": args.lambda_protein,
+ "lambda_rna": args.lambda_rna,
+ "lambda_phospho": args.lambda_phospho,
+ "normalize_fc_steady": args.normalize_fc_steady,
+ "use_initial_condition_from_data": args.use_initial_condition_from_data,
+ "scan": args.scan,
+ "sensitivity": args.sensitivity,
+ "model": args.model,
+ "model_code": args.model_code,
+ "time_points_protein": args.time_points_protein,
+ "time_points_rna": args.time_points_rna,
+ "time_points_phospho": args.time_points_phospho,
+ "n_starts": args.n_starts,
+ "profile_likelihood": args.profile_likelihood,
+ "posterior_sampling": args.posterior_sampling,
+ },
+ }
+
+
+def initialize_run_contract(args: argparse.Namespace) -> argparse.Namespace:
+ """Create output/provenance files after config and CLI precedence are resolved."""
+ from common.results import attach_file_console_logger, ensure_result_dir, write_command, write_metadata, \
+ write_resolved_config
+ from config.config import setup_logger
+
+ global logger
+ args.output_dir = str(ensure_result_dir(args.output_dir)["root"])
+ logger = setup_logger(log_dir=args.output_dir)
+ attach_file_console_logger(logger, args.output_dir)
+ write_command(args.output_dir)
+ write_resolved_config(args.output_dir, _jsonable_runtime(vars(args)))
+ write_metadata(
+ args.output_dir,
+ workflow="networkmodel.runner",
+ args=args,
+ inputs=[args.kinase_net, args.tf_net, args.ms, args.rna, args.phospho, args.kinopt, args.tfopt],
+ extra=_runtime_metadata_extra(args),
+ )
+ return args
+
+
+def _import_runtime_dependencies() -> None:
+ """Import networkmodel runtime modules only after --conf selected the config file."""
+ global np, pd, save_dashboard_bundle, run_hyperparameter_scan, run_sensitivity_analysis
+ global _dump_y0, build_W_parallel, build_tf_matrix, prepare_fast_loss_data, load_data
+ global Index, KinaseInput, System, GlobalODEScalarObjective, build_weight_functions
+ global init_raw_params, unpack_params, simulate_and_measure, normalize_fc_to_t0, _base_idx
+ global calculate_bio_bounds, get_optimized_sets, export_pareto_front_to_excel, plot_goodness_of_fit
+ global export_results, save_gene_timeseries_plots, export_S_rates, plot_s_rates_report
+ global export_kinase_activities, export_param_correlations, export_residuals, export_parameter_distributions
+ global simulate_until_steady, plot_steady_state_all, warn_deprecated_backend_options, detect_data_mode
+ global JaxoptResult, InferenceContext, run_multistart, run_profile_likelihood_standalone_processes
+ global run_numpyro_posterior_standalone_processes, configure_jax_parallelism, write_posterior_payload
+ global write_scalar_result_tables, frechet_distance, populate_standard_subdirs
+
+ import numpy as np
+ import pandas as pd
+ from common.frechet import frechet_distance
+ from common.results import populate_standard_subdirs
+ from networkmodel.BayesianInference import (
+ InferenceContext,
+ configure_jax_parallelism,
+ run_multistart,
+ run_numpyro_posterior_standalone_processes,
+ run_profile_likelihood_standalone_processes,
+ )
+ from networkmodel.BuildMatrix import build_W_parallel, build_tf_matrix
+ from networkmodel.InitialConditions import _dump_y0
+ from networkmodel.OptimizationProblem import GlobalODEScalarObjective, build_weight_functions
+ from networkmodel.PosteriorObjective import write_posterior_payload
+ from networkmodel.SteadyStateAnalysis import simulate_until_steady, plot_steady_state_all
+ from networkmodel.backend import JaxoptResult, detect_data_mode, warn_deprecated_backend_options
+ from networkmodel.cache import prepare_fast_loss_data
+ from networkmodel.dashboard_bundle import save_dashboard_bundle
+ from networkmodel.export import (
+ export_S_rates,
+ export_kinase_activities,
+ export_param_correlations,
+ export_pareto_front_to_excel,
+ export_parameter_distributions,
+ export_residuals,
+ export_results,
+ plot_goodness_of_fit,
+ plot_s_rates_report,
+ save_gene_timeseries_plots,
+ )
+ from networkmodel.io import load_data
+ from networkmodel.mode_outputs import write_scalar_result_tables
+ from networkmodel.network import Index, KinaseInput, System
+ from networkmodel.params import init_raw_params, unpack_params
+ from networkmodel.scan import run_hyperparameter_scan
+ from networkmodel.sensitivity import run_sensitivity_analysis
+ from networkmodel.simulate import simulate_and_measure
+ from networkmodel.utils import _base_idx, calculate_bio_bounds, get_optimized_sets, normalize_fc_to_t0
+
+
def main():
"""Run the networkmodel entry point
-
+
Returns:
Computed result from this routine.
-
+
Raises:
ValueError: When inputs are inconsistent or unsupported.
"""
- parser = argparse.ArgumentParser()
- parser.add_argument("--kinase-net", default=KINASE_NET_FILE)
- parser.add_argument("--tf-net", default=TF_NET_FILE)
- parser.add_argument("--ms", default=MS_DATA_FILE)
- parser.add_argument("--rna", default=RNA_DATA_FILE)
- parser.add_argument("--phospho", default=PHOSPHO_DATA_FILE)
-
- # kinopt and tfopt results
- parser.add_argument("--kinopt", default=KINOPT_RESULTS_FILE)
- parser.add_argument("--tfopt", default=TFOPT_RESULTS_FILE)
-
- parser.add_argument("--output-dir", default=RESULTS_DIR)
- parser.add_argument("--cores", type=int, default=CORES)
-
- # JAXopt
- parser.add_argument("--n-gen", type=int, default=MAX_ITERATIONS)
- parser.add_argument("--seed", type=int, default=SEED)
-
- # Loss weights
- parser.add_argument("--lambda-prior", type=float, default=REGULARIZATION_LAMBDA)
- parser.add_argument("--lambda-protein", type=float, default=REGULARIZATION_PROTEIN)
- parser.add_argument("--lambda-rna", type=float, default=REGULARIZATION_RNA)
- parser.add_argument("--lambda-phospho", type=float, default=REGULARIZATION_PHOSPHO)
-
- # Data inference
- parser.add_argument("--normalize-fc-steady", action="store_true", default=NORMALIZE_FC_STEADY)
- parser.add_argument("--use-initial-condition-from-data", action="store_true",
- default=USE_INITIAL_CONDITION_FROM_DATA)
- parser.add_argument("--scan", action="store_true",
- help="Run a hyperparameter scan using Optuna to find the best regularization parameters.",
- default=HYPERPARAM_SCAN)
- parser.add_argument("--sensitivity", action="store_true",
- help="Run a sensitivity analysis after optimization.",
- default=SENSITIVITY_ANALYSIS)
- parser.add_argument("--solver", type=str, choices=["jaxopt", "pymoo", "optuna"], default="jaxopt",
- help="Choice of optimization solver. Legacy pymoo/optuna values map to jaxopt.")
-
- args = parser.parse_args()
- os.makedirs(args.output_dir, exist_ok=True)
+ args, config_defaults = parse_runtime_args()
+ args = initialize_run_contract(args)
+ _import_runtime_dependencies()
# logger.info Arguments
logger.info("============================================================")
logger.info("PhosKinTime Global Model")
logger.info("------------------------------------------------------------")
- logger.info(f"Application : {APP_NAME}")
- logger.info(f"Version : {VERSION}")
- logger.info(f"Available Models : {AVAILABLE_MODELS}")
+ logger.info(f"Application : {args.app_name}")
+ logger.info(f"Version : {args.version}")
+ logger.info(f"Available Models : {args.available_models}")
logger.info("------------------------------------------------------------")
- logger.info(f"Parent Package : {PARENT_PACKAGE}")
- logger.info(f"Citation : {CITATION}")
- logger.info(f"DOI : {DOI}")
- logger.info(f"Source Code : {GITHUB_URL}")
- logger.info(f"Documentation : {DOCS_URL}")
+ logger.info(f"Parent Package : {args.parent_package}")
+ logger.info(f"Citation : {args.citation}")
+ logger.info(f"DOI : {args.doi}")
+ logger.info(f"Source Code : {args.github_url}")
+ logger.info(f"Documentation : {args.docs_url}")
logger.info("============================================================")
logger.info("[Solver] Using Diffrax Kvaerno Solver")
- if MODEL == 0:
+ if args.model_code == 0:
logger.info("[Model] Using Distributive Model")
- elif MODEL == 1:
+ elif args.model_code == 1:
logger.info("[Model] Using Sequential Model")
- elif MODEL == 2:
+ elif args.model_code == 2:
logger.info("[Model] Using Combinatorial Model")
- elif MODEL == 4:
+ elif args.model_code == 4:
logger.info("[Model] Using Saturating Model")
else:
- raise ValueError(f"Unknown MODEL value in config file: {MODEL}")
+ raise ValueError(f"Unknown MODEL value in config file: {args.model_code}")
logger.info(f"[Args] Output directory: {args.output_dir}")
logger.info(f"[Args] Number of cores: {args.cores}")
@@ -365,7 +578,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
)
# -----------------------------------------------------------------------------
- # FULL-UNIVERSE MODE (LEGACY / “MODEL EVERYTHING” BEHAVIOR)
+ # FULL-UNIVERSE MODE (LEGACY / “args.model_code EVERYTHING” BEHAVIOR)
# -----------------------------------------------------------------------------
# Rationale
# ---------
@@ -427,9 +640,9 @@ def _proxy_score(orphan: str, candidate: str) -> float:
# New: keep the config for logging, but don't treat it as a callable
weight_cfg = build_weight_functions(
- method_protein=WEIGHTING_METHOD_PROTEIN,
- method_rna=WEIGHTING_METHOD_RNA,
- time_grid=TIME_POINTS_PROTEIN,
+ method_protein=args.weighting_method_protein,
+ method_rna=args.weighting_method_rna,
+ time_grid=args.time_points_protein,
)
logger.info("[Weights] Protein weighting scheme: %s", weight_cfg["protein"])
@@ -439,11 +652,11 @@ def _proxy_score(orphan: str, candidate: str) -> float:
df_prot["w"] = 1.0
df_rna["w"] = 1.0
- # 3) Build W + TF
+ # 3) Build W + TF
# -------------------------------------------------------------------------
# Network Matrix Construction
# -------------------------------------------------------------------------
- # Build the kinase-substrate interaction matrix (W_global) and the
+ # Build the kinase-substrate interaction matrix (W_global) and the
# transcription factor regulatory matrix (tf_mat) in parallel for efficiency.
#
# W_global: Sparse matrix (sites × kinases) encoding kinase-substrate relationships
@@ -539,7 +752,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
logger.info("[Model] Initial conditions set from data.")
# 5) Precompute loss data on solver time grid
- solver_times = np.unique(np.concatenate([TIME_POINTS_PROTEIN, TIME_POINTS_RNA, TIME_POINTS_PHOSPHO]))
+ solver_times = np.unique(np.concatenate([args.time_points_protein, args.time_points_rna, args.time_points_phospho]))
loss_data = prepare_fast_loss_data(idx, df_prot, df_rna, df_pho, solver_times)
loss_data["prot_base_idx"] = _base_idx(solver_times, 0.0)
@@ -659,11 +872,50 @@ def _proxy_score(orphan: str, candidate: str) -> float:
xu=xu,
data_mode=mode,
)
+
+ n_protein_obs = int(loss_data.get("n_p", 0))
+ n_rna_obs = int(loss_data.get("n_r", 0))
+ n_phospho_obs = int(loss_data.get("n_ph", 0))
+ n_total_obs = n_protein_obs + n_rna_obs + n_phospho_obs
+
+ n_protein_entities = int(df_prot["protein"].nunique()) if "protein" in df_prot.columns else 0
+ n_rna_entities = int(df_rna["protein"].nunique()) if "protein" in df_rna.columns else 0
+
+ if {"protein", "psite"}.issubset(df_pho.columns):
+ n_phospho_sites = int(df_pho[["protein", "psite"]].drop_duplicates().shape[0])
+ elif "psite" in df_pho.columns:
+ n_phospho_sites = int(df_pho["psite"].nunique())
+ else:
+ n_phospho_sites = 0
+
+ n_protein_times = int(df_prot["time"].nunique()) if "time" in df_prot.columns else 0
+ n_rna_times = int(df_rna["time"].nunique()) if "time" in df_rna.columns else 0
+ n_phospho_times = int(df_pho["time"].nunique()) if "time" in df_pho.columns else 0
+
logger.info(
- f"[Data] Number of points: {loss_data['n_p']} protein, {loss_data['n_r']} RNA, "
- f"{loss_data['n_ph']} phospho | Total {loss_data['n_p'] + loss_data['n_r'] + loss_data['n_ph']} data points"
+ "[Data] Loss observations: "
+ "%d protein rows, %d RNA rows, %d phospho rows | Total %d observation rows",
+ n_protein_obs,
+ n_rna_obs,
+ n_phospho_obs,
+ n_total_obs,
)
+
+ logger.info(
+ "[Data] Loss entities: "
+ "%d protein entities × %d time points, "
+ "%d RNA entities × %d time points, "
+ "%d phosphosites × %d time points",
+ n_protein_entities,
+ n_protein_times,
+ n_rna_entities,
+ n_rna_times,
+ n_phospho_sites,
+ n_phospho_times,
+ )
+
configure_jax_parallelism(max_workers=args.cores, logger_obj=logger)
+
ctx = InferenceContext(
objective_fun=problem.objective,
theta0=theta0,
@@ -675,9 +927,9 @@ def _proxy_score(orphan: str, candidate: str) -> float:
maxiter=args.n_gen,
tol=1e-6,
)
- if N_STARTS > 1:
+ if args.n_starts > 1:
try:
- ms = run_multistart(ctx, n_starts=N_STARTS, seed=args.seed, max_workers=args.cores)
+ ms = run_multistart(ctx, n_starts=args.n_starts, seed=args.seed, max_workers=args.cores)
best_row = ms["best"].drop(
columns=["start_id", "seed", "success", "selected_best"],
errors="ignore",
@@ -721,11 +973,11 @@ def _proxy_score(orphan: str, candidate: str) -> float:
tol=1e-6,
)
- if PROFILE_LIKELIHOOD and PROFILE_INDICES.strip():
+ if args.profile_likelihood and args.profile_indices.strip():
try:
- logger.info("[Profile] Profile likelihood requested for indices: %s", PROFILE_INDICES)
+ logger.info("[Profile] Profile likelihood requested for indices: %s", args.profile_indices)
- profile_indices = [int(x) for x in PROFILE_INDICES.split(",") if x.strip()]
+ profile_indices = [int(x) for x in args.profile_indices.split(",") if x.strip()]
profile_run_config_path = write_posterior_payload(
ctx=ctx,
@@ -738,7 +990,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
run_config_path=profile_run_config_path,
output_dir=args.output_dir,
parameter_indices=profile_indices,
- grid_size=PROFILE_GRID_SIZE,
+ grid_size=args.profile_grid_size,
max_workers=1,
)
@@ -751,13 +1003,13 @@ def _proxy_score(orphan: str, candidate: str) -> float:
except Exception:
logger.exception("[Profile] Profile likelihood failed.")
- if POSTERIOR_SAMPLING:
+ if args.posterior_sampling:
try:
logger.info(
"[Posterior] Requested standalone posterior sampling "
"with warmup=%s, samples=%s.",
- POSTERIOR_NUM_WARMUP,
- POSTERIOR_NUM_SAMPLES,
+ args.posterior_num_warmup,
+ args.posterior_num_samples,
)
run_config_path = write_posterior_payload(
@@ -770,8 +1022,8 @@ def _proxy_score(orphan: str, candidate: str) -> float:
posterior_result = run_numpyro_posterior_standalone_processes(
run_config_path=run_config_path,
output_dir=args.output_dir,
- num_warmup=POSTERIOR_NUM_WARMUP,
- num_samples=POSTERIOR_NUM_SAMPLES,
+ num_warmup=args.posterior_num_warmup,
+ num_samples=args.posterior_num_samples,
seed=args.seed,
num_processes=4,
)
@@ -832,7 +1084,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
# Simulate with current parameters
dfp_temp, dfr_temp, dfph_temp = simulate_and_measure(
- sys, idx, TIME_POINTS_PROTEIN, TIME_POINTS_RNA, TIME_POINTS_PHOSPHO
+ sys, idx, args.time_points_protein, args.time_points_rna, args.time_points_phospho
)
detailed_scores = {"prot": {}, "rna": {}, "phospho": {}}
@@ -916,7 +1168,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
idx=idx,
fitted_params=params,
output_dir=args.output_dir,
- metric=SENSITIVITY_METRIC
+ metric=args.sensitivity_metric
)
# 1. Export Dynamic Kinase Activities (Mechanism check)
@@ -957,7 +1209,8 @@ def _proxy_score(orphan: str, candidate: str) -> float:
f"[Output] Saved phosphorylation rates report for picked solution {args.output_dir}/S_rates_report.pdf.")
# 12) Export picked solution
- dfp, dfr, dfph = simulate_and_measure(sys, idx, TIME_POINTS_PROTEIN, TIME_POINTS_RNA, TIME_POINTS_PHOSPHO)
+ dfp, dfr, dfph = simulate_and_measure(sys, idx, args.time_points_protein, args.time_points_rna,
+ args.time_points_phospho)
# Save raw preds
if dfp is not None: dfp.to_csv(os.path.join(args.output_dir, "pred_prot_picked.csv"), index=False)
@@ -992,8 +1245,8 @@ def _proxy_score(orphan: str, candidate: str) -> float:
df_phos_obs=df_pho,
df_phos_pred=dfph,
output_dir=ts_dir,
- prot_times=TIME_POINTS_PROTEIN,
- rna_times=TIME_POINTS_RNA,
+ prot_times=args.time_points_protein,
+ rna_times=args.time_points_rna,
filename_prefix="fit"
)
@@ -1038,8 +1291,8 @@ def _proxy_score(orphan: str, candidate: str) -> float:
logger.info("[Done] Exported results saved.")
- # Display all parameters from the configuration class
- global_config = load_config_toml("config.toml")
+ # Display all parameters from the selected configuration class
+ global_config = args.raw_config
logger.info("=" * 80)
logger.info("GLOBAL MODEL CONFIGURATION")
logger.info("=" * 80)
@@ -1120,6 +1373,7 @@ def _proxy_score(orphan: str, candidate: str) -> float:
logger.info(f"[Dashboard] Saved dashboard bundle: {bundle_path}")
# Finalize logging
+ populate_standard_subdirs(args.output_dir)
logger.info(f"[Complete] All results saved to: {args.output_dir}")
diff --git a/networkmodel/sensitivity.py b/networkmodel/sensitivity.py
index b1bf3bc..baa2ace 100644
--- a/networkmodel/sensitivity.py
+++ b/networkmodel/sensitivity.py
@@ -424,7 +424,7 @@ def run_sensitivity_analysis(sys, idx, fitted_params, output_dir, metric="l2_nor
mp_ctx = _get_process_context()
- with ProcessPoolExecutor(max_workers=n_workers, mp_context=mp_ctx) as executor:
+ with ProcessPoolExecutor(max_workers=n_workers, mp_context=mp_ctx, max_tasks_per_child=1) as executor:
futures = {
executor.submit(_worker_simulation, task): task[0]
for task in tasks
@@ -447,6 +447,13 @@ def run_sensitivity_analysis(sys, idx, fitted_params, output_dir, metric="l2_nor
"phos_df": dfph,
}
)
+ # Keep only the bounded set needed for identical top-curve
+ # reporting instead of retaining every trajectory DataFrame.
+ if len(trajectory_storage) > int(SENSITIVITY_TOP_CURVES):
+ trajectory_storage = _select_top_trajectories(
+ trajectory_storage,
+ max_items=SENSITIVITY_TOP_CURVES,
+ )
except Exception as exc:
failed_rows.append(
diff --git a/networkmodel/simulate.py b/networkmodel/simulate.py
index cd8b376..5dedcc9 100644
--- a/networkmodel/simulate.py
+++ b/networkmodel/simulate.py
@@ -11,6 +11,22 @@
from networkmodel.backend import DiffraxSolverConfig, make_networkmodel_rhs, solve_diffrax
+def combinatorial_site_signals_streaming(states, n_sites):
+ """Return per-site combinatorial signals without building an ns x n_sites bit matrix."""
+ state_view = np.asarray(states, dtype=np.float64)
+ if state_view.ndim != 2:
+ raise ValueError("states must be a two-dimensional time-by-state array")
+ ns = state_view.shape[1]
+ n_sites = int(n_sites)
+ out = np.empty((state_view.shape[0], n_sites), dtype=np.float64)
+ masks = np.arange(ns, dtype=np.uint64)
+ for site in range(n_sites):
+ weights = ((masks >> np.uint64(site)) & np.uint64(1)).astype(np.float64, copy=False)
+ out[:, site] = state_view @ weights
+ del weights
+ return out
+
+
def simulate_diffrax(sys, t_eval, rtol=None, atol=None, max_steps=None, solver_name="Kvaerno4"):
"""Simulate a System over requested time points with Diffrax
@@ -35,9 +51,19 @@ def simulate_diffrax(sys, t_eval, rtol=None, atol=None, max_steps=None, solver_n
)
params = (sys.c_k, sys.A_i, sys.B_i, sys.C_i, sys.D_i, sys.Dp_i, sys.E_i,
np.asarray([sys.tf_scale], dtype=np.float64))
+ rhs = getattr(sys, "_cached_jax_rhs", None)
+ if rhs is None:
+ rhs = make_networkmodel_rhs(sys)
+ sys._cached_jax_rhs = rhs
+
return np.asarray(
- solve_diffrax(y0, np.asarray(t_eval, dtype=np.float64), params=params, rhs=make_networkmodel_rhs(sys),
- config=cfg),
+ solve_diffrax(
+ y0,
+ np.asarray(t_eval, dtype=np.float64),
+ params=params,
+ rhs=rhs,
+ config=cfg,
+ ),
dtype=np.float64,
)
@@ -93,20 +119,20 @@ def _bidx(t0: float) -> int:
fc_p = np.maximum(tot, 1e-12) / np.maximum(tot[prot_b], 1e-12)
rows_p.append(pd.DataFrame({"protein": gene, "time": times, "pred_fc": fc_p}))
- # Phospho Sites: Bitwise aggregation
- # We map states to sites using a matrix multiplication (State x Bitmask)
+ # Phospho Sites: Bitwise aggregation, streamed one site at a time
+ # to avoid an ns x n_sites dense bit matrix.
if n_sites > 0:
- m = np.arange(ns, dtype=np.uint32)[:, None]
- j = np.arange(n_sites, dtype=np.uint32)[None, :]
- bits = ((m >> j) & 1).astype(np.float64) # (ns, n_sites)
- pho_sites = states @ bits # (T, n_sites)
-
+ masks = np.arange(ns, dtype=np.uint64)
for s_idx, psite in enumerate(idx.sites[i]):
- sig = pho_sites[:, s_idx]
+ weights = ((masks >> np.uint64(s_idx)) & np.uint64(1)).astype(np.float64, copy=False)
+ sig = states @ weights
fc = np.maximum(sig, 1e-12) / np.maximum(sig[pho_b], 1e-12)
rows_pho.append(pd.DataFrame({
"protein": gene, "psite": psite, "time": times, "pred_fc": fc
}))
+ del weights, sig
+ del masks
+ del states
else:
# --- Standard Model Extraction (Distributive/Sequential) ---
diff --git a/pixi.lock b/pixi.lock
index eccd9a2..7d1b83a 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -19,15 +19,37 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda
@@ -72,11 +94,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.7-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/git-cliff-2.13.1-h66dc0b5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-14.1.2-h8b86629_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.0-py312h8285ef7_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.52-ha5ea40c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda
@@ -86,14 +113,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jaxlib-0.9.2-cpu_py312hc81e8bd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -101,6 +131,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
@@ -110,18 +142,27 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.5-default_h746c552_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
@@ -139,16 +180,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.5-hf7376ad_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda
@@ -160,7 +207,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
@@ -174,6 +223,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/lineax-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.47.0-py312h7424e68_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda
@@ -190,6 +240,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.65.1-py312hd1dde6f_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.5-py312h33ff503_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -202,6 +253,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py312h8ecdadd_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hda50119_1.conda
@@ -217,11 +269,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -234,6 +291,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_3.conda
@@ -241,8 +299,11 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda
@@ -251,12 +312,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.49-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -267,10 +334,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda
@@ -301,6 +371,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/7d/da/dd2867c25adbb41563720f14b5fc895c98bf88be682a3faff4f7b3118d2a/igraph-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl
@@ -317,14 +388,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/atk-1.0-2.38.0-h4bec284_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda
@@ -366,11 +459,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/frozendict-2.4.7-py312h80b0991_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/git-cliff-2.13.1-h6d18f09_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/glib-tools-2.88.1-h6437393_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphviz-14.1.2-h44fc223_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/greenlet-3.5.0-py312h4075484_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gtk3-3.24.52-hf2d442a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gts-0.7.6-h53e17e3_4.conda
@@ -380,14 +478,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/hicolor-icon-theme-0.17-h694c41f_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jaxlib-0.9.0-cpu_py312hf3c3857_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -395,6 +496,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda
@@ -402,14 +505,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-7_he492b99_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-7_h9b27e0a_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda
@@ -420,13 +532,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgit2-1.9.3-h415d65b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.78.1-h147dede_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-7_h859234e_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h6e8c311_1.conda
@@ -434,7 +552,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda
@@ -445,6 +565,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.5-h0d3cbff_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.47.0-py312ha5a82fe_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda
@@ -461,6 +582,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.65.1-py312h704f9c4_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.4.5-py312h746d82c_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -471,6 +593,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-3.0.3-py312h8e27051_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-hf280016_1.conda
@@ -486,11 +609,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -502,14 +630,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312h2ac7433_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h77e0585_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.5.1-py312hb77ea7e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scikit-learn-1.8.0-np2py312h47bbdc5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.17.1-py312h6309490_0.conda
@@ -518,12 +649,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlalchemy-2.0.49-py312hba6025d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/statsmodels-0.14.6-py312h391ab28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -534,9 +671,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-2.1.2-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -545,6 +685,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda
- pypi: https://files.pythonhosted.org/packages/a5/03/3278ad0ceb3ea0e84d8ae3a85bdded4d0e57853aeb802a200feb43847b93/igraph-1.0.0-cp39-abi3-macosx_10_15_x86_64.whl
@@ -561,14 +702,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda
@@ -610,11 +773,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.7-py312h4409184_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-cliff-2.13.1-h5a0b6fd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.88.1-h37541a8_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/greenlet-3.5.0-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda
@@ -624,14 +792,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jaxlib-0.9.2-cpu_py312h8d61f43_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -639,6 +810,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.0-py312h3093aea_0.conda
@@ -646,14 +819,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hdfa7624_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
@@ -664,13 +846,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgit2-1.9.3-h9a1894c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.78.1-h3e3f78d_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-7_hd9741b5_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.33.5-h2d4b707_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h4c27e2a_1.conda
@@ -678,7 +866,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h5ef1a60_0.conda
@@ -689,6 +879,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.47.0-py312h7ca588d_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda
@@ -705,6 +896,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.65.1-py312h2d3d6e9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.4.5-py312ha003a3f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -714,6 +906,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-hf80efc4_1.conda
@@ -729,11 +922,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -745,14 +943,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-ha480c28_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.5.1-py312h8b1d842_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.8.0-np2py312he5ca3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.17.1-py312h0f234b1_0.conda
@@ -761,12 +962,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.49-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/statsmodels-0.14.6-py312ha11c99a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -777,9 +984,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.1-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hb3ab3e3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-2.1.2-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -788,6 +998,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/0d/bc/6281ec7f9baaf71ee57c3b1748da2d3148d15d253e1a03006f204aa68ca5/igraph-1.0.0-cp39-abi3-macosx_11_0_arm64.whl
@@ -814,7 +1025,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda
@@ -826,12 +1039,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda
@@ -883,11 +1115,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.7-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/git-cliff-2.13.1-h66dc0b5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-14.1.2-h8b86629_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.0-py312h8285ef7_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.52-ha5ea40c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda
@@ -897,18 +1134,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jaxlib-0.9.2-cpu_py312hc81e8bd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -932,7 +1173,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda
@@ -941,18 +1182,27 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.5-default_h746c552_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
@@ -970,16 +1220,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.5-hf7376ad_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda
@@ -991,7 +1247,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
@@ -1005,6 +1263,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/lineax-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.47.0-py312h7424e68_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda
@@ -1026,6 +1285,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.65.1-py312hd1dde6f_1.conda
@@ -1040,6 +1300,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py312h8ecdadd_0.conda
@@ -1058,14 +1319,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -1082,6 +1348,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda
@@ -1097,6 +1364,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda
@@ -1106,15 +1374,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.49-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -1127,15 +1401,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda
@@ -1165,6 +1442,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -1182,7 +1460,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
@@ -1193,12 +1473,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/atk-1.0-2.38.0-h4bec284_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda
@@ -1247,11 +1546,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/frozendict-2.4.7-py312h80b0991_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/git-cliff-2.13.1-h6d18f09_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/glib-tools-2.88.1-h6437393_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphviz-14.1.2-h44fc223_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/greenlet-3.5.0-py312h4075484_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gtk3-3.24.52-hf2d442a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gts-0.7.6-h53e17e3_4.conda
@@ -1261,18 +1565,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/hicolor-icon-theme-0.17-h694c41f_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jaxlib-0.9.0-cpu_py312hf3c3857_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -1296,21 +1604,30 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-7_he492b99_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-7_h9b27e0a_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda
@@ -1321,13 +1638,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgit2-1.9.3-h415d65b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.78.1-h147dede_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-7_h859234e_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h6e8c311_1.conda
@@ -1335,7 +1658,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda
@@ -1346,6 +1671,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.5-h0d3cbff_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.47.0-py312ha5a82fe_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda
@@ -1367,6 +1693,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.65.1-py312h704f9c4_1.conda
@@ -1379,6 +1706,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-3.0.3-py312h8e27051_0.conda
@@ -1397,14 +1725,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -1422,6 +1755,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda
@@ -1445,15 +1779,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlalchemy-2.0.49-py312hba6025d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/statsmodels-0.14.6-py312h391ab28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -1466,14 +1806,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-2.1.2-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda
@@ -1481,6 +1824,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -1498,7 +1842,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
@@ -1509,12 +1855,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda
@@ -1563,11 +1928,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.7-py312h4409184_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-cliff-2.13.1-h5a0b6fd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.88.1-h37541a8_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/greenlet-3.5.0-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.52-hc0f3e19_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda
@@ -1577,18 +1947,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jaxlib-0.9.2-cpu_py312h8d61f43_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -1612,21 +1986,30 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.0-py312h3093aea_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hdfa7624_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
@@ -1637,13 +2020,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgit2-1.9.3-h9a1894c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.78.1-h3e3f78d_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-7_hd9741b5_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.33.5-h2d4b707_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h4c27e2a_1.conda
@@ -1651,7 +2040,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h5ef1a60_0.conda
@@ -1662,6 +2053,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.47.0-py312h7ca588d_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda
@@ -1683,6 +2075,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.65.1-py312h2d3d6e9_1.conda
@@ -1694,6 +2087,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py312h6510ced_0.conda
@@ -1712,14 +2106,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -1737,6 +2136,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda
@@ -1760,15 +2160,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.49-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/statsmodels-0.14.6-py312ha11c99a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -1781,14 +2187,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.1-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hb3ab3e3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-2.1.2-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda
@@ -1796,6 +2205,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -1823,15 +2233,37 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda
@@ -1877,12 +2309,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.7-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/git-cliff-2.13.1-h66dc0b5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-14.1.2-h8b86629_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.0-py312h8285ef7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffe-2.0.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffecli-2.0.2-pyhcf101f3_0.conda
@@ -1895,14 +2332,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jaxlib-0.9.2-cpu_py312hc81e8bd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -1910,6 +2350,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
@@ -1919,18 +2361,27 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.5-default_h746c552_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
@@ -1948,16 +2399,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.5-hf7376ad_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda
@@ -1969,7 +2426,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
@@ -1983,6 +2442,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/lineax-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.47.0-py312h7424e68_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -2006,6 +2466,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.65.1-py312hd1dde6f_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.5-py312h33ff503_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -2018,6 +2479,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py312h8ecdadd_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hda50119_1.conda
@@ -2034,11 +2496,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -2052,6 +2519,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda
@@ -2060,8 +2528,11 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda
@@ -2070,12 +2541,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.49-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -2086,11 +2563,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda
@@ -2121,6 +2600,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl
@@ -2139,14 +2619,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/atk-1.0-2.38.0-h4bec284_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py312h4b46afd_1.conda
@@ -2189,12 +2691,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/frozendict-2.4.7-py312h80b0991_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/git-cliff-2.13.1-h6d18f09_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/glib-tools-2.88.1-h6437393_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphviz-14.1.2-h44fc223_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/greenlet-3.5.0-py312h4075484_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffe-2.0.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffecli-2.0.2-pyhcf101f3_0.conda
@@ -2207,14 +2714,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/hicolor-icon-theme-0.17-h694c41f_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jaxlib-0.9.0-cpu_py312hf3c3857_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -2222,6 +2732,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda
@@ -2229,14 +2741,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-7_he492b99_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-7_h9b27e0a_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda
@@ -2247,13 +2768,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgit2-1.9.3-h415d65b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.78.1-h147dede_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-7_h859234e_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h6e8c311_1.conda
@@ -2261,7 +2788,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda
@@ -2272,6 +2801,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.5-h0d3cbff_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.47.0-py312ha5a82fe_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -2295,6 +2825,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.65.1-py312h704f9c4_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.4.5-py312h746d82c_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -2305,6 +2836,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-3.0.3-py312h8e27051_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-hf280016_1.conda
@@ -2321,11 +2853,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -2338,6 +2875,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda
@@ -2345,8 +2883,10 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h77e0585_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.5.1-py312hb77ea7e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scikit-learn-1.8.0-np2py312h47bbdc5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.17.1-py312h6309490_0.conda
@@ -2355,12 +2895,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlalchemy-2.0.49-py312hba6025d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/statsmodels-0.14.6-py312h391ab28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -2371,10 +2917,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-2.1.2-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -2383,6 +2931,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda
- pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl
@@ -2401,14 +2950,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312h0dfefe5_1.conda
@@ -2451,12 +3022,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.7-py312h4409184_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-cliff-2.13.1-h5a0b6fd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.88.1-h37541a8_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gravis-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/greenlet-3.5.0-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffe-2.0.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/griffecli-2.0.2-pyhcf101f3_0.conda
@@ -2469,14 +3045,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jaxlib-0.9.2-cpu_py312h8d61f43_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -2484,6 +3063,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.0-py312h3093aea_0.conda
@@ -2491,14 +3072,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hdfa7624_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
@@ -2509,13 +3099,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgit2-1.9.3-h9a1894c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.78.1-h3e3f78d_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-7_hd9741b5_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.33.5-h2d4b707_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h4c27e2a_1.conda
@@ -2523,7 +3119,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h5ef1a60_0.conda
@@ -2534,6 +3132,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.47.0-py312h7ca588d_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -2557,6 +3156,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.65.1-py312h2d3d6e9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.4.5-py312ha003a3f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -2566,6 +3166,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/opt_einsum-3.4.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-hf80efc4_1.conda
@@ -2582,11 +3183,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyld-3.0.0-pyhcf101f3_0.conda
@@ -2599,6 +3205,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda
@@ -2606,8 +3213,10 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-ha480c28_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.5.1-py312h8b1d842_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.8.0-np2py312he5ca3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.17.1-py312h0f234b1_0.conda
@@ -2616,12 +3225,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.49-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/statsmodels-0.14.6-py312ha11c99a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -2632,10 +3247,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.1-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hb3ab3e3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-2.1.2-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -2644,6 +3261,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl
@@ -2672,7 +3290,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda
@@ -2684,12 +3304,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
@@ -2742,9 +3381,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.7-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/git-cliff-2.13.1-h66dc0b5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-14.1.2-h8b86629_0.conda
@@ -2761,18 +3404,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jaxlib-0.9.2-cpu_py312hc81e8bd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -2796,7 +3443,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda
@@ -2805,18 +3452,27 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.5-default_h746c552_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
@@ -2834,16 +3490,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.5-hf7376ad_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda
@@ -2855,7 +3517,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
@@ -2869,6 +3533,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/lineax-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.47.0-py312h7424e68_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -2897,6 +3562,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.65.1-py312hd1dde6f_1.conda
@@ -2912,6 +3578,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py312h8ecdadd_0.conda
@@ -2931,14 +3598,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pygraphviz-1.14-py312hcdbcef4_3.conda
@@ -2957,6 +3629,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda
@@ -2973,6 +3646,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda
@@ -2982,15 +3656,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.49-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -3003,6 +3683,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda
@@ -3011,8 +3692,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda
@@ -3042,6 +3724,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -3061,7 +3744,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
@@ -3072,12 +3757,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/atk-1.0-2.38.0-h4bec284_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
@@ -3127,9 +3831,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/frozendict-2.4.7-py312h80b0991_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/git-cliff-2.13.1-h6d18f09_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/glib-tools-2.88.1-h6437393_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphviz-14.1.2-h44fc223_0.conda
@@ -3146,18 +3854,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/hicolor-icon-theme-0.17-h694c41f_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jaxlib-0.9.0-cpu_py312hf3c3857_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -3181,21 +3893,30 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h207b36a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-7_he492b99_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-7_h9b27e0a_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda
@@ -3206,13 +3927,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgit2-1.9.3-h415d65b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.78.1-h147dede_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-7_h859234e_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h6e8c311_1.conda
@@ -3220,7 +3947,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda
@@ -3231,6 +3960,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.5-h0d3cbff_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.47.0-py312ha5a82fe_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -3259,6 +3989,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.65.1-py312h704f9c4_1.conda
@@ -3272,6 +4003,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-3.0.3-py312h8e27051_0.conda
@@ -3291,14 +4023,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pygraphviz-1.14-py312h9f547f1_3.conda
@@ -3318,6 +4055,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda
@@ -3342,15 +4080,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlalchemy-2.0.49-py312hba6025d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/statsmodels-0.14.6-py312h391ab28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -3363,6 +4107,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda
@@ -3370,8 +4115,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-2.1.2-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.12-h8616949_1.conda
@@ -3379,6 +4125,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -3398,7 +4145,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda
@@ -3409,12 +4158,31 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda
@@ -3464,9 +4232,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.7-py312h4409184_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-cliff-2.13.1-h5a0b6fd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.88.1-h37541a8_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda
@@ -3483,18 +4255,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jaxlib-0.9.2-cpu_py312h8d61f43_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -3518,21 +4294,30 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.8-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.0-py312h3093aea_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hdfa7624_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
@@ -3543,13 +4328,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgit2-1.9.3-h9a1894c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.78.1-h3e3f78d_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-7_hd9741b5_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.33.5-h2d4b707_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h4c27e2a_1.conda
@@ -3557,7 +4348,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h5ef1a60_0.conda
@@ -3568,6 +4361,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.47.0-py312h7ca588d_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.10.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
@@ -3596,6 +4390,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.65.1-py312h2d3d6e9_1.conda
@@ -3608,6 +4403,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py312h6510ced_0.conda
@@ -3627,14 +4423,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pygraphviz-1.14-py312h0c23599_3.conda
@@ -3654,6 +4455,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda
@@ -3678,15 +4480,21 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.4-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.49-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/statsmodels-0.14.6-py312ha11c99a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -3699,6 +4507,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.1-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hb3ab3e3_3.conda
@@ -3706,8 +4515,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-2.1.2-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda
@@ -3715,6 +4525,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl
@@ -3744,15 +4555,37 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.15.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
@@ -3798,8 +4631,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.7-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.6-h2b0a6b4_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/git-cliff-2.13.1-h66dc0b5_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-14.1.2-h8b86629_0.conda
@@ -3813,14 +4650,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/jaxlib-0.9.2-cpu_py312hc81e8bd_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -3828,6 +4668,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
@@ -3837,18 +4679,27 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-7_h0358290_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-22.1.5-default_h746c552_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda
@@ -3866,16 +4717,22 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-7_h47877c9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm22-22.1.5-hf7376ad_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.33-pthreads_h94d23a6_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.4-hd5a49e9_0.conda
@@ -3887,7 +4744,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.341.0-h5279c79_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda
@@ -3901,6 +4760,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/lineax-0.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.47.0-py312h7424e68_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.1.0-py312h63ddcf0_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda
@@ -3917,6 +4777,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.65.1-py312hd1dde6f_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.5-py312h33ff503_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -3930,6 +4791,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py312h8ecdadd_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hda50119_1.conda
@@ -3945,11 +4807,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pygraphviz-1.14-py312hcdbcef4_3.conda
@@ -3963,6 +4830,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_3.conda
@@ -3970,8 +4838,11 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.1-pl5321h16c4a6b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.5.1-py312h192e038_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda
@@ -3980,12 +4851,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.49-py312h5253ce2_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -3996,10 +4873,13 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h20c3967_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.25.0-hd6090a7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda
@@ -4030,6 +4910,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda
- pypi: https://files.pythonhosted.org/packages/7d/da/dd2867c25adbb41563720f14b5fc895c98bf88be682a3faff4f7b3118d2a/igraph-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl
@@ -4046,14 +4927,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/atk-1.0-2.38.0-h4bec284_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
@@ -4096,8 +4999,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/frozendict-2.4.7-py312h80b0991_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.6-hae309b2_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/git-cliff-2.13.1-h6d18f09_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/glib-tools-2.88.1-h6437393_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/graphviz-14.1.2-h44fc223_0.conda
@@ -4111,14 +5018,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/hicolor-icon-theme-0.17-h694c41f_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/jaxlib-0.9.0-cpu_py312hf3c3857_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -4126,6 +5036,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.5.0-py312hb1dc2e7_0.conda
@@ -4133,14 +5045,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.19.1-h5ea7634_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20260107.1-cxx17_h7ed6875_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.11.0-7_he492b99_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.11.0-7_h9b27e0a_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.0-hcc62823_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_0.conda
@@ -4151,13 +5072,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-15.2.0-hd16e46c_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgit2-1.9.3-h415d65b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.1-hf28f236_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.78.1-h147dede_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.4.1-ha1e9b39_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.11.0-7_h859234e_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.33-openmp_h9e49c7b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.33.5-hff14b61_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2025.11.05-h6e8c311_1.conda
@@ -4165,7 +5092,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsodium-1.0.22-ha3d0635_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.1-h8f8c405_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.1-ha0a348c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-16-2.15.3-h7a90416_0.conda
@@ -4176,6 +5105,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.5-h0d3cbff_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/llvmlite-0.47.0-py312ha5a82fe_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/lxml-6.1.0-py312h211e60a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py312heb39f77_1.conda
@@ -4192,6 +5122,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numba-0.65.1-py312h704f9c4_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/numpy-2.4.5-py312h746d82c_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -4203,6 +5134,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-3.0.3-py312h8e27051_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-hf280016_1.conda
@@ -4218,11 +5150,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py312hf7082af_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pygraphviz-1.14-py312h9f547f1_3.conda
@@ -4235,14 +5172,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py312h51361c1_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/pyzmq-27.1.0-py312h2ac7433_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2025.11.05-h77e0585_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.5.1-py312hb77ea7e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scikit-learn-1.8.0-np2py312h47bbdc5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.17.1-py312h6309490_0.conda
@@ -4251,12 +5191,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/sqlalchemy-2.0.49-py312hba6025d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/statsmodels-0.14.6-py312h391ab28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/tornado-6.5.5-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -4267,9 +5213,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/unicodedata2-17.0.1-py312h1a1c95f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-6.0.0-py312hba6025d_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/wrapt-2.1.2-py312h933eb07_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -4278,6 +5227,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zeromq-4.3.5-h84953be_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-ng-2.3.3-h8bce59a_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda
- pypi: https://files.pythonhosted.org/packages/a5/03/3278ad0ceb3ea0e84d8ae3a85bdded4d0e57853aeb802a200feb43847b93/igraph-1.0.0-cp39-abi3-macosx_10_15_x86_64.whl
@@ -4294,14 +5244,36 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/alive-progress-3.3.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/autograd-1.8.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda
@@ -4344,8 +5316,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.7-py312h4409184_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.6-h4e57454_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-cliff-2.13.1-h5a0b6fd_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.88.1-h37541a8_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-14.1.2-hec8c438_0.conda
@@ -4359,14 +5335,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyh5552912_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.14.1-pyh53cf698_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jaxlib-0.9.2-cpu_py312h8d61f43_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jaxopt-0.8.4-pyhd8ed1ab_0.conda
@@ -4374,6 +5353,8 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.9.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.0-py312h3093aea_0.conda
@@ -4381,14 +5362,23 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hdfa7624_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260107.1-cxx17_h2062a1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-7_h51639a9_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-7_hb0561ab_openblas.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.5-h55c6f16_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.0-hf6b4638_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda
@@ -4399,13 +5389,19 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgit2-1.9.3-h9a1894c_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.1-ha08bb59_2.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.78.1-h3e3f78d_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.4.1-h84a0fba_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-7_hd9741b5_openblas.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.33-openmp_he657e61_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.33.5-h2d4b707_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h4c27e2a_1.conda
@@ -4413,7 +5409,9 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.3-h5ef1a60_0.conda
@@ -4424,6 +5422,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.5-hc7d1edf_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvmlite-0.47.0-py312h7ca588d_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.1.0-py312h2f8615f_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda
@@ -4440,6 +5439,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numba-0.65.1-py312h2d3d6e9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.4.5-py312ha003a3f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/numpyro-0.21.0-pyhd8ed1ab_0.conda
@@ -4450,6 +5450,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/optimistix-0.1.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-4.8.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/optuna-dashboard-0.20.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py312h6510ced_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-hf80efc4_1.conda
@@ -4465,11 +5466,16 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/plotly-6.6.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pox-0.3.7-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ppft-1.7.8-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pygraphviz-1.14-py312h0c23599_3.conda
@@ -4482,14 +5488,17 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.13-hd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-graphviz-0.21-pyhbacfb6d_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-ha480c28_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.5.1-py312h8b1d842_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.8.0-np2py312he5ca3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.17.1-py312h0f234b1_0.conda
@@ -4498,12 +5507,18 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-81.0.0-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.49-py312hb3ab3e3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/statsmodels-0.14.6-py312ha11c99a_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.5-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda
@@ -4514,9 +5529,12 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-17.0.1-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wadler-lindig-0.1.7-pyhe01879c_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hb3ab3e3_3.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.1-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-2.1.2-py312h2bbb03f_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/xlsxwriter-3.2.9-pyhd8ed1ab_0.conda
@@ -4525,6 +5543,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda
- pypi: https://files.pythonhosted.org/packages/0d/bc/6281ec7f9baaf71ee57c3b1748da2d3148d15d253e1a03006f204aa68ca5/igraph-1.0.0-cp39-abi3-macosx_11_0_arm64.whl
@@ -4668,6 +5687,23 @@ packages:
purls: []
size: 584660
timestamp: 1768327524772
+- conda: https://conda.anaconda.org/conda-forge/noarch/altair-6.2.1-pyhd8ed1ab_0.conda
+ sha256: a420cfb87d5688a00fd859c30115ff85528a0cc0b899b419537b259bb62a26d5
+ md5: ba28d9dfeb5bfc14d26f2730ed8f13d8
+ depends:
+ - importlib-metadata
+ - jinja2
+ - jsonschema >=3.0
+ - narwhals >=2.4.0
+ - packaging
+ - python >=3.10
+ - typing-extensions >=4.12.0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/altair?source=compressed-mapping
+ size: 570625
+ timestamp: 1780924043343
- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.4-pyhcf101f3_0.conda
sha256: cc9fbc50d4ee7ee04e49ee119243e6f1765750f0fd0b4d270d5ef35461b643b1
md5: 52be5139047efadaeeb19c6a5103f92a
@@ -4680,6 +5716,18 @@ packages:
- pkg:pypi/annotated-doc?source=hash-mapping
size: 14222
timestamp: 1762868213144
+- conda: https://conda.anaconda.org/conda-forge/noarch/ansi2html-1.9.2-pyhcf101f3_3.conda
+ sha256: aa38131a1a861fa24b9ef61fb4a2412b81039e47bc714c030ee9d524fc0b7da4
+ md5: d8573ef104c250314148c020f4c552e4
+ depends:
+ - python >=3.10
+ - python
+ license: LGPL-3.0-or-later
+ license_family: LGPL
+ purls:
+ - pkg:pypi/ansi2html?source=hash-mapping
+ size: 35961
+ timestamp: 1757744058909
- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda
sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e
md5: af2df4b9108808da3dc76710fe50eae2
@@ -4907,186 +5955,944 @@ packages:
- pkg:pypi/autograd?source=hash-mapping
size: 47679
timestamp: 1746451563674
-- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
- sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770
- md5: f1976ce927373500cc19d3c0b2c85177
- depends:
- - python >=3.10
- - python
- constrains:
- - pytz >=2015.7
- license: BSD-3-Clause
- license_family: BSD
- purls:
- - pkg:pypi/babel?source=hash-mapping
- size: 7684321
- timestamp: 1772555330347
-- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
- sha256: a2b08a4e5e549b5f67c38edffd175437e2208547a7e67b5fa5373b67ef419e50
- md5: b31dba71fe091e7201826e57e0f7b261
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hea842a7_2.conda
+ sha256: 5b9c20a38fe084b4ffd1f2c64b3797ec6ef9a99e83cc0c1f84e016c9801e3a5c
+ md5: c463d2dbfb12a208c943165d2a568db4
depends:
- - python
- - libgcc >=14
- __glibc >=2.17,<3.0.a0
- - zstd >=1.5.7,<1.6.0a0
- - python_abi 3.12.* *_cp312
- license: BSD-3-Clause AND MIT AND EPL-2.0
- purls:
- - pkg:pypi/backports-zstd?source=hash-mapping
- size: 239928
- timestamp: 1778594049826
-- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
- sha256: f6166347ee8dd7e5c71029c27e89cc5d4a84ccb3f374761363d5bce08ce92227
- md5: c987aba92ae6e528ed495f1ea71d4834
+ - libgcc >=14
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 134385
+ timestamp: 1780598328124
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.10.3-h2dfa1e0_2.conda
+ sha256: 25f88f6ab63db63ef3011084cee06c62bfadde169a630a16588b21d6969320a2
+ md5: 512f46909e6c405c20728918f60851b8
depends:
- - python
- __osx >=11.0
- - python_abi 3.12.* *_cp312
- - zstd >=1.5.7,<1.6.0a0
- license: BSD-3-Clause AND MIT AND EPL-2.0
- purls:
- - pkg:pypi/backports-zstd?source=hash-mapping
- size: 240621
- timestamp: 1778594129022
-- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
- sha256: a492dcf07b1c58797b3192f11aef7e3beb18ec91646d6a5acfe5c6e61e66118d
- md5: 6ec306e02579965dc9c01092a5f4ce4c
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 120720
+ timestamp: 1780598468278
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hceed5df_2.conda
+ sha256: b4689664156e8067ba1aa97125f2a309a96b2bc0d1c608f4a88f30ea1f4c9aba
+ md5: e7501df14d3145fc86943ebfeb76a402
depends:
- - python
- __osx >=11.0
- - zstd >=1.5.7,<1.6.0a0
- - python_abi 3.12.* *_cp312
- license: BSD-3-Clause AND MIT AND EPL-2.0
- purls:
- - pkg:pypi/backports-zstd?source=compressed-mapping
- size: 240840
- timestamp: 1778594074672
-- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
- sha256: aed4b9dcf68ec2a75e5645fed14d77fd884d38d2e52bfa6ef4b278d90cd88781
- md5: 3b261da3fe9b4168738712832410b022
- depends:
- - python >=3.10
- - soupsieve >=1.2
- - typing-extensions
- license: MIT
- license_family: MIT
- purls:
- - pkg:pypi/beautifulsoup4?source=hash-mapping
- size: 92704
- timestamp: 1780853175566
-- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
- sha256: 93b3f5782a46773e6e16d96c3fcd37f52a4ec0f8c4c28ec2427596b1dc7a2a7c
- md5: e6a300dfd0e778f3f7c8a25b8894c56f
- depends:
- - python >=3.10
- - httpx >=0.22.0
- - pandas >=1.1.5
- - pyld >=0.7.2
- - python
- license: BSD-2-Clause
- license_family: BSD
- purls:
- - pkg:pypi/biothings-client?source=hash-mapping
- size: 38022
- timestamp: 1775024557715
-- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
- sha256: 0c786f3e571bd58ac73d730d06314716663884d848ae320de0b438fae5e0bea9
- md5: 93009c29cdd6f2500468f2502fff9209
- depends:
- - python >=3.10
- - webencodings
- - python
- constrains:
- - tinycss2 >=1.1.0,<1.5
- license: Apache-2.0 AND MIT
- purls:
- - pkg:pypi/bleach?source=compressed-mapping
- size: 142246
- timestamp: 1780675823953
-- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
- sha256: ede77e412304cd080e23967352a7904932207d0167ecdccd6a9e210530942be6
- md5: 5f710eab1f3c4e773c75686f5e8e6481
- depends:
- - bleach ==6.4.0 pyhcf101f3_0
- - tinycss2
- license: Apache-2.0 AND MIT
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
purls: []
- size: 4406
- timestamp: 1780675823953
-- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
- sha256: 2a7c5d93059cc656de4af20d533f91fcf3aa063596f95aa93245553249b69030
- md5: ddb34ae9ba36f9479bf605302adf54e2
- depends:
- - python >=3.9
- - python
- license: MIT
- license_family: MIT
- purls:
- - pkg:pypi/bottle?source=hash-mapping
- size: 58208
- timestamp: 1754876199648
-- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
- sha256: e511644d691f05eb12ebe1e971fd6dc3ae55a4df5c253b4e1788b789bdf2dfa6
- md5: 8ccf913aaba749a5496c17629d859ed1
+ size: 116718
+ timestamp: 1780598398659
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h78948cc_2.conda
+ sha256: 06a0e2af439b21c94adff8fac5dd66dbda5f182fc80ac635c4903959ea306cbb
+ md5: fe81235aae00f32df8584267b4f2daf8
depends:
- __glibc >=2.17,<3.0.a0
- - brotli-bin 1.2.0 hb03c661_1
- - libbrotlidec 1.2.0 hb03c661_1
- - libbrotlienc 1.2.0 hb03c661_1
+ - aws-c-common >=0.14.0,<0.14.1.0a0
- libgcc >=14
- license: MIT
- license_family: MIT
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: Apache
purls: []
- size: 20103
- timestamp: 1764017231353
-- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
- sha256: c838c71ded28ada251589f6462fc0f7c09132396799eea2701277566a1a863bf
- md5: 149d8ee7d6541a02a6117d8814fd9413
+ size: 57011
+ timestamp: 1780566647051
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.9.14-hcb77be1_2.conda
+ sha256: d36ca9a9d031d381f2270480d834833e0fdb71d4793307b0a11b0ed7e45b63a0
+ md5: 18708874716ed71706c80769e8ba5409
depends:
- - __osx >=10.13
- - brotli-bin 1.2.0 h8616949_1
- - libbrotlidec 1.2.0 h8616949_1
- - libbrotlienc 1.2.0 h8616949_1
- license: MIT
- license_family: MIT
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: Apache
purls: []
- size: 20194
- timestamp: 1764017661405
-- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
- sha256: 422ac5c91f8ef07017c594d9135b7ae068157393d2a119b1908c7e350938579d
- md5: 48ece20aa479be6ac9a284772827d00c
+ size: 45674
+ timestamp: 1780567082039
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h81c6212_2.conda
+ sha256: 557bc47cbfd01dc569b930c102cd56ca5ba67750bd51a4fcee445246e7e536cd
+ md5: dcac0aa854a1f7f58a59226f5309a44e
depends:
- __osx >=11.0
- - brotli-bin 1.2.0 hc919400_1
- - libbrotlidec 1.2.0 hc919400_1
- - libbrotlienc 1.2.0 hc919400_1
- license: MIT
- license_family: MIT
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: Apache
purls: []
- size: 20237
- timestamp: 1764018058424
-- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
- sha256: 64b137f30b83b1dd61db6c946ae7511657eead59fdf74e84ef0ded219605aa94
- md5: af39b9a8711d4a8d437b52c1d78eb6a1
+ size: 45764
+ timestamp: 1780567235337
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.0-hb03c661_0.conda
+ sha256: 6d2b33965bf6daeffd3ad336f41410053ff06ed6f2b2ce62c1ec27c0a39b4e7e
+ md5: f1c005b2e3b618706112ddd7f3af4521
depends:
- __glibc >=2.17,<3.0.a0
- - libbrotlidec 1.2.0 hb03c661_1
- - libbrotlienc 1.2.0 hb03c661_1
- libgcc >=14
- license: MIT
- license_family: MIT
+ license: Apache-2.0
+ license_family: Apache
purls: []
- size: 21021
- timestamp: 1764017221344
-- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
- sha256: dcb5a2b29244b82af2545efad13dfdf8dddb86f88ce64ff415be9e7a10cc0383
- md5: 34803b20dfec7af32ba675c5ccdbedbf
+ size: 242497
+ timestamp: 1780160843944
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.14.0-ha1e9b39_0.conda
+ sha256: c07dca511740b30b3bb26d9d5d14ce2577e65c422bc0afb875581792242a4514
+ md5: 983f44cf7123c92ddbb19e9398f577ea
depends:
- - __osx >=10.13
- - libbrotlidec 1.2.0 h8616949_1
- - libbrotlienc 1.2.0 h8616949_1
- license: MIT
- license_family: MIT
+ - __osx >=11.0
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 232296
+ timestamp: 1780161157428
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.0-h84a0fba_0.conda
+ sha256: 223f67551038366555e6934802d8b375547b142157aad3fc3654c720ac1525c0
+ md5: 3a49923f2b3987a833a264caca603f84
+ depends:
+ - __osx >=11.0
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 226438
+ timestamp: 1780161234587
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haa0cbde_2.conda
+ sha256: 0e4952f9be8de7f281ca7d734a3a8f05ad0db856c6ef1e0897798c4afbcd9a54
+ md5: 595911421e25551e36fde7027bf33f38
+ depends:
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 22007
+ timestamp: 1780566239465
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.3.2-ha04291d_2.conda
+ sha256: 7e3de1e42fb88192f1e39bb3d9024d3b228ad06b94508056d0d2175448387706
+ md5: a7163d39a3e639901fc1ce4865e11b47
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 21517
+ timestamp: 1780566351431
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h61d3404_2.conda
+ sha256: 4289ff476103d109623bd413b12d61307d6267e87fc6a8c29b0aec71dfa8fd84
+ md5: 497edff11fcb32865d8c5d6ab3aef6e0
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 21529
+ timestamp: 1780566290492
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h9cf6be0_2.conda
+ sha256: 6b893ba3173206e17fef1b9c8b683c6f6ecbca7127770c8d0f3ba13d123f8d4c
+ md5: 2c304605f9074f072c92c0d8de175a1a
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 59271
+ timestamp: 1780586883495
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.7.1-hf02f33c_2.conda
+ sha256: 52166148575189fb6fcbe272900ab3e1066cbf2af6e2d81d4408fe366211dc54
+ md5: ea1fd47007bf4362c1d17e388af42479
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 54060
+ timestamp: 1780586926676
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h7e6a3cf_2.conda
+ sha256: 5e0c69837e21fc17cc26ad6c252e842a96bb16f5be2c6f06f48a13b8a56fc56f
+ md5: 608685880a69722c685d1729c57409f6
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 53730
+ timestamp: 1780586998748
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h6488f85_2.conda
+ sha256: d2b844db1a4dfbc20b5129b7df4a656c1459c5fb16745101bbd802813ba8d411
+ md5: da0be1e8cb4a43c876f26d9d812dea06
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - aws-c-compression >=0.3.2,<0.3.3.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 230293
+ timestamp: 1780586764553
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.11.0-he315c99_2.conda
+ sha256: 181d69666b6d7dab3669c2bf964971495c0b1dfa6a5823bf0626d8f53e1f56fb
+ md5: aa2b61bf50c3c666683488fef3187436
+ depends:
+ - __osx >=11.0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-compression >=0.3.2,<0.3.3.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 197085
+ timestamp: 1780586807052
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-h0a63974_2.conda
+ sha256: 06d3b08ed19cd63fd75750e325f19ebf7183b22ee27cbe2ca7b7dd6725d34885
+ md5: f0fc8139091eb8245209bb9ee8911a82
+ depends:
+ - __osx >=11.0
+ - aws-c-compression >=0.3.2,<0.3.3.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 177282
+ timestamp: 1780586850553
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-h5b668fc_4.conda
+ sha256: c27b972325342f062da00b7aa2d5abf6f3ce55668a05703a175be958745cc226
+ md5: 555400dce62f2d989ff77761c010d166
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - s2n >=1.7.3,<1.7.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 181627
+ timestamp: 1780575920109
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.26.3-hd35ae92_4.conda
+ sha256: 14903b20e23b9dbf8fc828ad1bffb46b68f1b100aee4a10beb6fbb5eb0068288
+ md5: 3888bd82cc3a8f6bfa8ae0e4261b69cb
+ depends:
+ - __osx >=11.0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 182726
+ timestamp: 1780575986786
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h58c0f83_4.conda
+ sha256: 18f51bdc45eabe01ca68edf5ccc73369b3201639790575e6776f3efaea6e4356
+ md5: b33f51eca94f6ccbd772ca4043fe1718
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 176913
+ timestamp: 1780576001260
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.15.2-h0d2f46f_4.conda
+ sha256: c81aa872f74baf6bd2bb82cc87815895b3a654ef7831177a5c3d851ee27c05ea
+ md5: c66a882d62800e451a651b9b002f5066
+ depends:
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 221640
+ timestamp: 1780598982374
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.15.2-h60a7cf6_4.conda
+ sha256: 6d035740e2a61a8bdec8405c68d78e5ac7e23582071bb6fc82d83f34191db5b6
+ md5: bfdfb69208c68204ebe3fefa640efb32
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 193321
+ timestamp: 1780599069085
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.15.2-ha70999f_4.conda
+ sha256: ab15db26173d775b92503808bd4c29bfca484d5feb6b639793f8adba3004c56e
+ md5: 24f47ec268da87f530058df459de3dad
+ depends:
+ - __osx >=11.0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 156284
+ timestamp: 1780599082085
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.5-hb916526_1.conda
+ sha256: 8f193173f1dccb25ff86a95543db27f0c762cccfe93157b02cf20bd5b4c11a92
+ md5: 70bc8e5e8cefd19407423733e7ebf540
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - openssl >=3.5.6,<4.0a0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 153453
+ timestamp: 1780609553521
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.12.5-h8cc6e82_1.conda
+ sha256: 2077da563f7e81f007a4eac4b233931c8500b3ca3aae50ef37001fa90e133792
+ md5: 75914204f2c708212f2185abeca539b4
+ depends:
+ - __osx >=11.0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 135785
+ timestamp: 1780609654545
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.5-h43def2a_1.conda
+ sha256: 0a99b506bbe21f00f21047db50b2eea2ff8a0b1146ff0fba7d04b39a568453f4
+ md5: 7dc63973f9fe772985b8c2f8ba5958ce
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-checksums >=0.2.10,<0.2.11.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 132141
+ timestamp: 1780609600116
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-haa0cbde_6.conda
+ sha256: 123c4325b16f1f6db95846d51e5e4201399ee29c0325f8b5a8db2e7d732b9151
+ md5: 4b66ac29a7e917a629b790c3d239d110
+ depends:
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 59085
+ timestamp: 1780568538653
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.2.4-ha04291d_6.conda
+ sha256: 44bca0a25e978729b995f2f265e0576d32292a4cc23953beafa233fec8f6184e
+ md5: 2d3f039770cab013521cc78e84b34e64
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 55961
+ timestamp: 1780568586569
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.4-h61d3404_6.conda
+ sha256: ef53cd1e30bc8c865c44df6f097f36361945665157e63957d68fe90aa7e4d66c
+ md5: 127bce41f9e6cc3bdb9e6daed95896d9
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 53659
+ timestamp: 1780568618924
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haa0cbde_2.conda
+ sha256: ad49333d96a5f9bcce02752a6515cbb077d7513e358a8fb1a832f4e772d54bac
+ md5: 5c05a63452bf73c50aa272a6f961c4fc
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 101627
+ timestamp: 1780568539
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.2.10-ha04291d_2.conda
+ sha256: 5ba7da95d95800d1fcd21397a7ddcea505faee420b2efb21b35cd12a50ad7154
+ md5: 81edba692bcff370dbf8e64660097c8d
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 96023
+ timestamp: 1780568602293
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h61d3404_2.conda
+ sha256: 9af1483700bb29870297e2390838d3c31293e8cf80fd8a8a9bd9a1446020a8d8
+ md5: 7c5f6a6efce80e728c1f743e064ab657
+ depends:
+ - __osx >=11.0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 91975
+ timestamp: 1780568646105
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.0-h41299d8_1.conda
+ sha256: cc8eade570327e97ea458dd47dad9845fce5be070024a3fdaffe5aa4f68d2126
+ md5: b4fbfcaea33878c12f0e035f5814280b
+ depends:
+ - libstdcxx >=14
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ - aws-c-mqtt >=0.15.2,<0.15.3.0a0
+ - aws-c-s3 >=0.12.5,<0.12.6.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 415624
+ timestamp: 1780917918279
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.40.0-h29c3229_1.conda
+ sha256: 9592201c5e533e031542fc06c546afb1535b7731a11828d7fd24a8df2717ffa4
+ md5: 5f3b48a9b1420e24f156f2aab77cb6fa
+ depends:
+ - libcxx >=19
+ - __osx >=11.0
+ - aws-c-s3 >=0.12.5,<0.12.6.0a0
+ - aws-c-mqtt >=0.15.2,<0.15.3.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 352519
+ timestamp: 1780918017140
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.0-hd6eb0f7_1.conda
+ sha256: 258cea4855f3d289dce09ab197bf2abfb5e983fefce371e4d100ec1a8d015277
+ md5: 522e7961ac3402ab3814d9759f7c54de
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ - aws-c-auth >=0.10.3,<0.10.4.0a0
+ - aws-c-cal >=0.9.14,<0.9.15.0a0
+ - aws-c-s3 >=0.12.5,<0.12.6.0a0
+ - aws-c-sdkutils >=0.2.4,<0.2.5.0a0
+ - aws-c-io >=0.26.3,<0.26.4.0a0
+ - aws-c-http >=0.11.0,<0.11.1.0a0
+ - aws-c-mqtt >=0.15.2,<0.15.3.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 275283
+ timestamp: 1780917960902
+- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.747-h6154047_6.conda
+ sha256: 224c461787555e92a1111b8a5c7f65db0559da631f05b0e29d06e79a267cc130
+ md5: 9096d36ad4522d330bd6a5bdbd458275
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 3624840
+ timestamp: 1781003610286
+- conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.747-h6b5c32a_6.conda
+ sha256: 6e94795256fded99749f3e76ed98c5e5b289d2d64ef53b5ac0c3e424c97c261c
+ md5: 472743e866a6dbff31a9b784be804501
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - libzlib >=1.3.2,<2.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 3477227
+ timestamp: 1781003677904
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.747-h55dad5a_6.conda
+ sha256: 9fa8fcc0da0b26269e488f8db252d416062671b55fbb57bc81c049343567ac37
+ md5: 391aa9618724ce3a08901de5ae43c447
+ depends:
+ - libcxx >=19
+ - __osx >=11.0
+ - libcurl >=8.20.0,<9.0a0
+ - aws-c-event-stream >=0.7.1,<0.7.2.0a0
+ - aws-c-common >=0.14.0,<0.14.1.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 3261009
+ timestamp: 1781003677069
+- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda
+ sha256: 321d1070905e467b6bc6f5067b97c1868d7345c272add82b82e08a0224e326f0
+ md5: 5492abf806c45298ae642831c670bba0
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libcurl >=8.18.0,<9.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - openssl >=3.5.4,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 348729
+ timestamp: 1768837519361
+- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.16.2-h87f1c7e_0.conda
+ sha256: bc2cde0d7204b3574084de1d83d80bceb7eb1550a17a0f0ccedbb312145475d3
+ md5: 24997c4c96d1875956abd9ce37f262eb
+ depends:
+ - __osx >=10.13
+ - libcurl >=8.18.0,<9.0a0
+ - libcxx >=19
+ - openssl >=3.5.4,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 298273
+ timestamp: 1768837905794
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.2-he5ae378_0.conda
+ sha256: d9a04af33d9200fcd9f6c954e2a882c5ac78af4b82025623e59cb7f7e590b451
+ md5: 7efe92d28599c224a24de11bb14d395e
+ depends:
+ - __osx >=11.0
+ - libcurl >=8.18.0,<9.0a0
+ - libcxx >=19
+ - openssl >=3.5.4,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 290928
+ timestamp: 1768837810218
+- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda
+ sha256: 2beb6ae8406f946b8963a67e72fe74453e1411c5ae7e992978340de6c512d13c
+ md5: 68bfb556bdf56d56e9f38da696e752ca
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 250511
+ timestamp: 1770344967948
+- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.13.3-h1135191_1.conda
+ sha256: 182769c18c23e2b29bb35f6fca4c233f0125f84418dacb2c36912298dafbe42e
+ md5: 14d2491d2dfcbb127fa0ff6219704ab5
+ depends:
+ - __osx >=10.13
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libcxx >=19
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 175167
+ timestamp: 1770345309347
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h810541e_1.conda
+ sha256: 428fa73808a688a252639080b6751953ad7ecd8a4cbd8f23147b954d6902b31b
+ md5: ca46cc84466b5e05f15a4c4f263b6e80
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libcxx >=19
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 167424
+ timestamp: 1770345338067
+- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.17.0-hf824e48_1.conda
+ sha256: be3680fb1ee53451383a942ce70e2463c97d278eadd8b7251f27241d48a12eea
+ md5: 7fffabaef945a7d1794dfe884cc71d2f
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 587104
+ timestamp: 1778840673576
+- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.17.0-hefc3566_1.conda
+ sha256: bb0b60f062a30eb46c84f09ed6266a4fd2550aa9fe38902668e18409861cb26f
+ md5: 462274475c7e0de7b4e3e4bd600c8383
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libcxx >=19
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 442119
+ timestamp: 1778841001503
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.17.0-h5446563_1.conda
+ sha256: 006adead59236b7bbf55da1e98c8a5147312b2eebd13f6f1be334a1c10cd8c59
+ md5: 64665660d15e88c0214007f57e4cbe36
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libcxx >=19
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 434440
+ timestamp: 1778841366650
+- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.13.0-ha7a2c86_0.conda
+ sha256: 67fa6937bc2f6400f5ff19727f5d926fdc68d7fce3aaeab4016f49bb93d89cbb
+ md5: a7e8cca395e0a1616b389749580b7804
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - libxml2
+ - libxml2-16 >=2.14.6
+ - openssl >=3.5.6,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 159140
+ timestamp: 1778661935076
+- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.13.0-h74781cd_0.conda
+ sha256: 21cf4bc77e20a4a4874452dc5438fdae86f2cccfa2ffa29e920b2be0450e906b
+ md5: 7d4ec20278fbc5159c0899787a8afea3
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libcxx >=19
+ - libxml2
+ - libxml2-16 >=2.14.6
+ - openssl >=3.5.6,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 133457
+ timestamp: 1778662369219
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.13.0-he467506_0.conda
+ sha256: bc73ce983d90baa732e6f64e4d8b4ddbb8e671c5d6e7b9475d33dbd118ddd5b6
+ md5: 4cfc08976cf62fef7736a763652987cb
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - libcxx >=19
+ - libxml2
+ - libxml2-16 >=2.14.6
+ - openssl >=3.5.6,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 128808
+ timestamp: 1778662321258
+- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.15.0-h1e5b466_0.conda
+ sha256: b6d0c80a01c9c3d652b47fd894ce32bcb2aad49829824a63235ede9375b8ae25
+ md5: c9186aa979528963657db3e63c25e987
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 303841
+ timestamp: 1778870507280
+- conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.15.0-haae7687_0.conda
+ sha256: a409db604fef0edb99b9bf9f3208f64c433184686f890a4adb2db46ca0e4ada3
+ md5: db657cfeb64d33244726cf1b30930edd
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libcxx >=19
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 208419
+ timestamp: 1778871005257
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.15.0-hfea7fb9_0.conda
+ sha256: 75a7567556dc579ac2a6e07f7046b4ca1a18871aa207351d1e9dff6be0770d03
+ md5: cdd2b09a96d66def91b0539282803cfc
+ depends:
+ - __osx >=11.0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-common-cpp >=12.13.0,<12.13.1.0a0
+ - libcxx >=19
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 201824
+ timestamp: 1778871097416
+- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda
+ sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770
+ md5: f1976ce927373500cc19d3c0b2c85177
+ depends:
+ - python >=3.10
+ - python
+ constrains:
+ - pytz >=2015.7
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/babel?source=hash-mapping
+ size: 7684321
+ timestamp: 1772555330347
+- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda
+ sha256: a2b08a4e5e549b5f67c38edffd175437e2208547a7e67b5fa5373b67ef419e50
+ md5: b31dba71fe091e7201826e57e0f7b261
+ depends:
+ - python
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - zstd >=1.5.7,<1.6.0a0
+ - python_abi 3.12.* *_cp312
+ license: BSD-3-Clause AND MIT AND EPL-2.0
+ purls:
+ - pkg:pypi/backports-zstd?source=hash-mapping
+ size: 239928
+ timestamp: 1778594049826
+- conda: https://conda.anaconda.org/conda-forge/osx-64/backports.zstd-1.5.0-py312h5f4ecc6_0.conda
+ sha256: f6166347ee8dd7e5c71029c27e89cc5d4a84ccb3f374761363d5bce08ce92227
+ md5: c987aba92ae6e528ed495f1ea71d4834
+ depends:
+ - python
+ - __osx >=11.0
+ - python_abi 3.12.* *_cp312
+ - zstd >=1.5.7,<1.6.0a0
+ license: BSD-3-Clause AND MIT AND EPL-2.0
+ purls:
+ - pkg:pypi/backports-zstd?source=hash-mapping
+ size: 240621
+ timestamp: 1778594129022
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda
+ sha256: a492dcf07b1c58797b3192f11aef7e3beb18ec91646d6a5acfe5c6e61e66118d
+ md5: 6ec306e02579965dc9c01092a5f4ce4c
+ depends:
+ - python
+ - __osx >=11.0
+ - zstd >=1.5.7,<1.6.0a0
+ - python_abi 3.12.* *_cp312
+ license: BSD-3-Clause AND MIT AND EPL-2.0
+ purls:
+ - pkg:pypi/backports-zstd?source=compressed-mapping
+ size: 240840
+ timestamp: 1778594074672
+- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda
+ sha256: aed4b9dcf68ec2a75e5645fed14d77fd884d38d2e52bfa6ef4b278d90cd88781
+ md5: 3b261da3fe9b4168738712832410b022
+ depends:
+ - python >=3.10
+ - soupsieve >=1.2
+ - typing-extensions
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/beautifulsoup4?source=hash-mapping
+ size: 92704
+ timestamp: 1780853175566
+- conda: https://conda.anaconda.org/conda-forge/noarch/biothings_client-0.5.0-pyhcf101f3_0.conda
+ sha256: 93b3f5782a46773e6e16d96c3fcd37f52a4ec0f8c4c28ec2427596b1dc7a2a7c
+ md5: e6a300dfd0e778f3f7c8a25b8894c56f
+ depends:
+ - python >=3.10
+ - httpx >=0.22.0
+ - pandas >=1.1.5
+ - pyld >=0.7.2
+ - python
+ license: BSD-2-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/biothings-client?source=hash-mapping
+ size: 38022
+ timestamp: 1775024557715
+- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.4.0-pyhcf101f3_0.conda
+ sha256: 0c786f3e571bd58ac73d730d06314716663884d848ae320de0b438fae5e0bea9
+ md5: 93009c29cdd6f2500468f2502fff9209
+ depends:
+ - python >=3.10
+ - webencodings
+ - python
+ constrains:
+ - tinycss2 >=1.1.0,<1.5
+ license: Apache-2.0 AND MIT
+ purls:
+ - pkg:pypi/bleach?source=compressed-mapping
+ size: 142246
+ timestamp: 1780675823953
+- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.4.0-hac0b51c_0.conda
+ sha256: ede77e412304cd080e23967352a7904932207d0167ecdccd6a9e210530942be6
+ md5: 5f710eab1f3c4e773c75686f5e8e6481
+ depends:
+ - bleach ==6.4.0 pyhcf101f3_0
+ - tinycss2
+ license: Apache-2.0 AND MIT
+ purls: []
+ size: 4406
+ timestamp: 1780675823953
+- conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda
+ sha256: f7efd22b5c15b400ed84a996d777b6327e5c402e79e3c534a7e086236f1eb2dc
+ md5: 42834439227a4551b939beeeb8a4b085
+ depends:
+ - python >=3.9
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/blinker?source=hash-mapping
+ size: 13934
+ timestamp: 1731096548765
+- conda: https://conda.anaconda.org/conda-forge/noarch/bottle-0.13.4-pyhe01879c_0.conda
+ sha256: 2a7c5d93059cc656de4af20d533f91fcf3aa063596f95aa93245553249b69030
+ md5: ddb34ae9ba36f9479bf605302adf54e2
+ depends:
+ - python >=3.9
+ - python
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/bottle?source=hash-mapping
+ size: 58208
+ timestamp: 1754876199648
+- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda
+ sha256: e511644d691f05eb12ebe1e971fd6dc3ae55a4df5c253b4e1788b789bdf2dfa6
+ md5: 8ccf913aaba749a5496c17629d859ed1
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - brotli-bin 1.2.0 hb03c661_1
+ - libbrotlidec 1.2.0 hb03c661_1
+ - libbrotlienc 1.2.0 hb03c661_1
+ - libgcc >=14
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 20103
+ timestamp: 1764017231353
+- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.2.0-hf139dec_1.conda
+ sha256: c838c71ded28ada251589f6462fc0f7c09132396799eea2701277566a1a863bf
+ md5: 149d8ee7d6541a02a6117d8814fd9413
+ depends:
+ - __osx >=10.13
+ - brotli-bin 1.2.0 h8616949_1
+ - libbrotlidec 1.2.0 h8616949_1
+ - libbrotlienc 1.2.0 h8616949_1
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 20194
+ timestamp: 1764017661405
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda
+ sha256: 422ac5c91f8ef07017c594d9135b7ae068157393d2a119b1908c7e350938579d
+ md5: 48ece20aa479be6ac9a284772827d00c
+ depends:
+ - __osx >=11.0
+ - brotli-bin 1.2.0 hc919400_1
+ - libbrotlidec 1.2.0 hc919400_1
+ - libbrotlienc 1.2.0 hc919400_1
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 20237
+ timestamp: 1764018058424
+- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda
+ sha256: 64b137f30b83b1dd61db6c946ae7511657eead59fdf74e84ef0ded219605aa94
+ md5: af39b9a8711d4a8d437b52c1d78eb6a1
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libbrotlidec 1.2.0 hb03c661_1
+ - libbrotlienc 1.2.0 hb03c661_1
+ - libgcc >=14
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 21021
+ timestamp: 1764017221344
+- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.2.0-h8616949_1.conda
+ sha256: dcb5a2b29244b82af2545efad13dfdf8dddb86f88ce64ff415be9e7a10cc0383
+ md5: 34803b20dfec7af32ba675c5ccdbedbf
+ depends:
+ - __osx >=10.13
+ - libbrotlidec 1.2.0 h8616949_1
+ - libbrotlienc 1.2.0 h8616949_1
+ license: MIT
+ license_family: MIT
purls: []
size: 18589
timestamp: 1764017635544
@@ -6182,6 +7988,40 @@ packages:
purls: []
size: 548309
timestamp: 1774986047281
+- conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda
+ sha256: 6c33bf0c4d8f418546ba9c250db4e4221040936aef8956353bc764d4877bc39a
+ md5: d411fc29e338efb48c5fd4576d71d881
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=13
+ - libstdcxx >=13
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 119654
+ timestamp: 1726600001928
+- conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda
+ sha256: c0bea66f71a6f4baa8d4f0248e17f65033d558d9e882c0af571b38bcca3e4b46
+ md5: a26de8814083a6971f14f9c8c3cb36c2
+ depends:
+ - __osx >=10.13
+ - libcxx >=17
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 84946
+ timestamp: 1726600054963
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda
+ sha256: fd56ed8a1dab72ab90d8a8929b6f916a6d9220ca297ff077f8f04c5ed3408e20
+ md5: 57a511a5905caa37540eb914dfcbf1fb
+ depends:
+ - __osx >=11.0
+ - libcxx >=17
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 82090
+ timestamp: 1726600145480
- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda
sha256: 40fdf5a9d5cc7a3503cd0c33e1b90b1e6eab251aaaa74e6b965417d089809a15
md5: 93f742fe078a7b34c29a182958d4d765
@@ -6237,6 +8077,31 @@ packages:
purls: []
size: 4889040
timestamp: 1777214915812
+- conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda
+ sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3
+ md5: 7c14f3706e099f8fcd47af2d494616cc
+ depends:
+ - python >=3.9
+ - smmap >=3.0.1,<6
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/gitdb?source=hash-mapping
+ size: 53136
+ timestamp: 1735887290843
+- conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.50-pyhd8ed1ab_0.conda
+ sha256: 718c9d0cc287ffda978996c4105ddd2863dd7bad7fb5794cdd365bd809ef2fa5
+ md5: 98958318a01373a615f119b1040b3027
+ depends:
+ - gitdb >=4.0.1,<5
+ - python >=3.10
+ - typing_extensions >=3.10.0.2
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/gitpython?source=hash-mapping
+ size: 162193
+ timestamp: 1778065820061
- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.88.1-hee1de02_2.conda
sha256: ae41fd5c867bc4e713a8cc1dc06f5b418026fec116cc222abe33e94235c6b241
md5: e5a459d2bb98edb88de5a44bfad66b9d
@@ -6273,6 +8138,42 @@ packages:
purls: []
size: 204902
timestamp: 1778508895255
+- conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda
+ sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2
+ md5: ff862eebdfeb2fd048ae9dc92510baca
+ depends:
+ - gflags >=2.2.2,<2.3.0a0
+ - libgcc-ng >=12
+ - libstdcxx-ng >=12
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 143452
+ timestamp: 1718284177264
+- conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda
+ sha256: dd56547db8625eb5c91bb0a9fbe8bd6f5c7fbf5b6059d46365e94472c46b24f9
+ md5: 06cf91665775b0da395229cd4331b27d
+ depends:
+ - __osx >=10.13
+ - gflags >=2.2.2,<2.3.0a0
+ - libcxx >=16
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 117017
+ timestamp: 1718284325443
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda
+ sha256: 9fc77de416953aa959039db72bc41bfa4600ae3ff84acad04a7d0c1ab9552602
+ md5: fef68d0a95aa5b84b5c1a4f6f3bf40e1
+ depends:
+ - __osx >=11.0
+ - gflags >=2.2.2,<2.3.0a0
+ - libcxx >=16
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 112215
+ timestamp: 1718284365403
- conda: https://conda.anaconda.org/conda-forge/noarch/graphemeu-0.7.2-pyhc364b38_0.conda
sha256: a240fa87f988d73863e5f0e49abc85103cba8bd8caf3d1df92c8999a26a50ef2
md5: 794487e5353f8ea5bf3b6bb55e460791
@@ -6748,9 +8649,50 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/httpcore?source=hash-mapping
- size: 49483
- timestamp: 1745602916758
+ - pkg:pypi/httpcore?source=hash-mapping
+ size: 49483
+ timestamp: 1745602916758
+- conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h4c3975b_0.conda
+ sha256: c2a9f3bdc3708555c0e7d4125d0c544a3ddfad75b86d683ce7903778b326bb43
+ md5: b71b691f15371ef1628fc6229df9fd39
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/httptools?source=compressed-mapping
+ size: 103105
+ timestamp: 1779757517448
+- conda: https://conda.anaconda.org/conda-forge/osx-64/httptools-0.8.0-py312h933eb07_0.conda
+ sha256: 8399c7ac507dbb18c425684e362f58e76980bf8df48701ca2751f40f013d8d5c
+ md5: 1e730af039a43e2a85ec730c326f6d89
+ depends:
+ - __osx >=11.0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/httptools?source=hash-mapping
+ size: 91728
+ timestamp: 1779757774471
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/httptools-0.8.0-py312h2bbb03f_0.conda
+ sha256: e9f593b0480db19f92b5f51990d2144f4192cc49b67e180f8b49ca5dcd3f839b
+ md5: 6059c5018f728489df82e9f5647eb4d0
+ depends:
+ - __osx >=11.0
+ - python >=3.12,<3.13.0a0
+ - python >=3.12,<3.13.0a0 *_cpython
+ - python_abi 3.12.* *_cp312
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/httptools?source=hash-mapping
+ size: 92162
+ timestamp: 1779757961181
- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda
sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950
md5: d6989ead454181f4f9bc987d3dc4e285
@@ -6920,6 +8862,19 @@ packages:
- sphinx-gallery>=0.14.0 ; extra == 'doc'
- pydoctor>=23.4.0 ; extra == 'doc'
requires_python: '>=3.9'
+- conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda
+ sha256: 8ef69fa00c68fad34a3b7b260ea774fda9bd9274fd706d3baffb9519fd0063fe
+ md5: b5577bc2212219566578fd5af9993af6
+ depends:
+ - numpy
+ - pillow >=8.3.2
+ - python >=3.9
+ license: BSD-2-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/imageio?source=hash-mapping
+ size: 293226
+ timestamp: 1738273949742
- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda
sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16
md5: 080594bf4493e6bae2607e65390c520a
@@ -7037,6 +8992,17 @@ packages:
- pkg:pypi/ipython?source=hash-mapping
size: 652893
timestamp: 1780654403616
+- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-pyhd8ed1ab_2.conda
+ sha256: 45821a8986b4cb2421f766b240dbe6998a3c3123f012dd566720c1322e9b6e18
+ md5: 2f0ba4bc12af346bc6c99bdc377e8944
+ depends:
+ - python >=3.9
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/ipython-genutils?source=hash-mapping
+ size: 28153
+ timestamp: 1733399692864
- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda
sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104
md5: bd80ba060603cc228d9d81c257093119
@@ -7049,22 +9015,23 @@ packages:
- pkg:pypi/ipython-pygments-lexers?source=hash-mapping
size: 13993
timestamp: 1737123723464
-- conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda
- sha256: 6bb58afb7eabc8b4ac0c7e92707fb498313cc0164cf04e7ba1090dbf49af514b
- md5: d68e3f70d1f068f1b66d94822fdc644e
+- conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-7.8.5-pyhd8ed1ab_0.conda
+ sha256: 8cc67e44137bb779c76d92952fdc4d8cd475605f4f0d13e8d0f04f25c056939b
+ md5: 47672c493015ab57d5fcde9531ab18ef
depends:
- comm >=0.1.3
- - ipython >=6.1.0
- - jupyterlab_widgets >=3.0.15,<3.1.0
- - python >=3.10
+ - ipython >=4.0.0
+ - ipython_genutils >=0.2.0,<0.3.0
+ - jupyterlab_widgets >=1.0.0,<3
+ - python >=3.3
- traitlets >=4.3.1
- - widgetsnbextension >=4.0.14,<4.1.0
+ - widgetsnbextension >=3.6.10,<3.7.0
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/ipywidgets?source=hash-mapping
- size: 114376
- timestamp: 1762040524661
+ size: 104860
+ timestamp: 1729599554932
- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda
sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed
md5: 0b0154421989637d424ccf0f104be51a
@@ -7077,6 +9044,17 @@ packages:
- pkg:pypi/isoduration?source=hash-mapping
size: 19832
timestamp: 1733493720346
+- conda: https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.2.0-pyhd8ed1ab_1.conda
+ sha256: 1684b7b16eec08efef5302ce298c606b163c18272b69a62b666fbaa61516f170
+ md5: 7ac5f795c15f288984e32add616cdc59
+ depends:
+ - python >=3.9
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/itsdangerous?source=hash-mapping
+ size: 19180
+ timestamp: 1733308353037
- conda: https://conda.anaconda.org/conda-forge/noarch/jax-0.9.0-pyhd8ed1ab_0.conda
sha256: 35935c2883ce138f2f8ae87cd6a30723838a8b7b54f5be71d6698399f2c61941
md5: eb556c579bf757f95f65f01146d99e92
@@ -7543,20 +9521,19 @@ packages:
- pkg:pypi/jupyterlab-server?source=hash-mapping
size: 51621
timestamp: 1761145478692
-- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda
- sha256: 5c03de243d7ae6247f39a402f4785d95e61c3be79ef18738e8f17155585d31a8
- md5: dbf8b81974504fa51d34e436ca7ef389
+- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-1.1.11-pyhd8ed1ab_0.conda
+ sha256: 639544e96969c7513b33bf3201a4dc3095625e34cff16c187f5dec9bee2dfb2f
+ md5: 05a08b368343304618b6a88425aa851a
depends:
- - python >=3.10
- - python
+ - python >=3.7
constrains:
- jupyterlab >=3,<5
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/jupyterlab-widgets?source=hash-mapping
- size: 216779
- timestamp: 1762267481404
+ size: 113654
+ timestamp: 1729586559116
- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda
sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4
md5: b38117a3c920364aff79f870c984b4a3
@@ -7793,6 +9770,338 @@ packages:
purls: []
size: 1229639
timestamp: 1770863511331
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-h8ff9baf_5_cpu.conda
+ build_number: 5
+ sha256: 0720ac20811d24386ae49f12818955a4f8bf8614248eaf8710661d113e69dad0
+ md5: c53e71fb57a7bc476d9d39b3b20ff01f
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ - aws-sdk-cpp >=1.11.747,<1.11.748.0a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-identity-cpp >=1.13.3,<1.13.4.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-files-datalake-cpp >=12.15.0,<12.15.1.0a0
+ - bzip2 >=1.0.8,<2.0a0
+ - glog >=0.7.1,<0.8.0a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libbrotlidec >=1.2.0,<1.3.0a0
+ - libbrotlienc >=1.2.0,<1.3.0a0
+ - libgcc >=14
+ - libgoogle-cloud >=3.5.0,<3.6.0a0
+ - libgoogle-cloud-storage >=3.5.0,<3.6.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libstdcxx >=14
+ - libzlib >=1.3.2,<2.0a0
+ - lz4-c >=1.10.0,<1.11.0a0
+ - orc >=2.3.0,<2.3.1.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ constrains:
+ - apache-arrow-proc =*=cpu
+ - parquet-cpp <0.0a0
+ - arrow-cpp <0.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 6502448
+ timestamp: 1781069558661
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-24.0.0-h9e06b3e_5_cpu.conda
+ build_number: 5
+ sha256: 1a1f6f149ef2f6dfcabbdef6f91497304f506bbf228e939cd3cc3b0db635fb48
+ md5: 379fbe62b5aec5d09d8a3a6390d405da
+ depends:
+ - __osx >=11.0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ - aws-sdk-cpp >=1.11.747,<1.11.748.0a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-identity-cpp >=1.13.3,<1.13.4.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-files-datalake-cpp >=12.15.0,<12.15.1.0a0
+ - bzip2 >=1.0.8,<2.0a0
+ - glog >=0.7.1,<0.8.0a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libbrotlidec >=1.2.0,<1.3.0a0
+ - libbrotlienc >=1.2.0,<1.3.0a0
+ - libcxx >=21
+ - libgoogle-cloud >=3.5.0,<3.6.0a0
+ - libgoogle-cloud-storage >=3.5.0,<3.6.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - lz4-c >=1.10.0,<1.11.0a0
+ - orc >=2.3.0,<2.3.1.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ constrains:
+ - parquet-cpp <0.0a0
+ - arrow-cpp <0.0a0
+ - apache-arrow-proc =*=cpu
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 4379910
+ timestamp: 1781071412907
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-h91214ac_5_cpu.conda
+ build_number: 5
+ sha256: 4385e30de42c00b1b9af19c8739cdbd071db681efa5565834d582da1dfdf6b9f
+ md5: 966e0004af993d89ab1c3419907cd121
+ depends:
+ - __osx >=11.0
+ - aws-crt-cpp >=0.40.0,<0.40.1.0a0
+ - aws-sdk-cpp >=1.11.747,<1.11.748.0a0
+ - azure-core-cpp >=1.16.2,<1.16.3.0a0
+ - azure-identity-cpp >=1.13.3,<1.13.4.0a0
+ - azure-storage-blobs-cpp >=12.17.0,<12.17.1.0a0
+ - azure-storage-files-datalake-cpp >=12.15.0,<12.15.1.0a0
+ - bzip2 >=1.0.8,<2.0a0
+ - glog >=0.7.1,<0.8.0a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libbrotlidec >=1.2.0,<1.3.0a0
+ - libbrotlienc >=1.2.0,<1.3.0a0
+ - libcxx >=21
+ - libgoogle-cloud >=3.5.0,<3.6.0a0
+ - libgoogle-cloud-storage >=3.5.0,<3.6.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - lz4-c >=1.10.0,<1.11.0a0
+ - orc >=2.3.0,<2.3.1.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ constrains:
+ - arrow-cpp <0.0a0
+ - apache-arrow-proc =*=cpu
+ - parquet-cpp <0.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 4243237
+ timestamp: 1781071364885
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_5_cpu.conda
+ build_number: 5
+ sha256: 4a1fd8e83ad082e272b141a004126a32b5224a5db5fd644c49a298e4897790e7
+ md5: dc27f196e5195d49b05b85a085c93cbe
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libarrow 24.0.0 h8ff9baf_5_cpu
+ - libarrow-compute 24.0.0 h53684a4_5_cpu
+ - libgcc >=14
+ - libstdcxx >=14
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 591772
+ timestamp: 1781069800881
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-24.0.0-h91633f5_5_cpu.conda
+ build_number: 5
+ sha256: 8d8aabe8eb2bda6731659bbe5a4943e8eaf36834df27a846bc428271d3e53ad2
+ md5: 979ccf6d3b2c82e4955afc972aa03660
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h9e06b3e_5_cpu
+ - libarrow-compute 24.0.0 hb38465b_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 543993
+ timestamp: 1781072348441
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-ha4f4840_5_cpu.conda
+ build_number: 5
+ sha256: f27843d66a5bb27616746a1aaf62b71887b01251b09bc1f8794fc5ef298fda66
+ md5: 80ca2ca7d2d34e25fc7397610ebef405
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h91214ac_5_cpu
+ - libarrow-compute 24.0.0 h8d10c55_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 519823
+ timestamp: 1781072090303
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_5_cpu.conda
+ build_number: 5
+ sha256: 0649d1cee07d5a357cb43886bc8a11d8e128ebf5e28bdebe1f9c52f46ba417a6
+ md5: 1715725b98c02d522654e46ad4995711
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libarrow 24.0.0 h8ff9baf_5_cpu
+ - libgcc >=14
+ - libre2-11 >=2025.11.5
+ - libstdcxx >=14
+ - libutf8proc >=2.11.3,<2.12.0a0
+ - re2
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 2990989
+ timestamp: 1781069683178
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-compute-24.0.0-hb38465b_5_cpu.conda
+ build_number: 5
+ sha256: 2b88a48288f7e0646dee7804eef37026ecd1040102d4008034f5ef141afa7d7f
+ md5: 3e72d9888a970f0f62663ff371e94c40
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h9e06b3e_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libre2-11 >=2025.11.5
+ - libutf8proc >=2.11.3,<2.12.0a0
+ - re2
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 2386508
+ timestamp: 1781071790833
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h8d10c55_5_cpu.conda
+ build_number: 5
+ sha256: c72e24ae9ac552db7282cb9c3be0802efdfd244e1fcbf79bf6b81aa7eeab1d55
+ md5: a1c861ce771d8706e3ac7c56786556ca
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h91214ac_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libre2-11 >=2025.11.5
+ - libutf8proc >=2.11.3,<2.12.0a0
+ - re2
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 2240777
+ timestamp: 1781071586006
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_5_cpu.conda
+ build_number: 5
+ sha256: 808e63b6321a6fba8f6a8abf2d9e15a6463b850b2f963004f99114df789c2ad8
+ md5: 57ced4ba426e4d34da4ce4b3c8154f06
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libarrow 24.0.0 h8ff9baf_5_cpu
+ - libarrow-acero 24.0.0 h635bf11_5_cpu
+ - libarrow-compute 24.0.0 h53684a4_5_cpu
+ - libgcc >=14
+ - libparquet 24.0.0 h7376487_5_cpu
+ - libstdcxx >=14
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 592166
+ timestamp: 1781069880155
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-24.0.0-h91633f5_5_cpu.conda
+ build_number: 5
+ sha256: 1c63b0aabc202738ea8ac16006bc699a7d02da8064a017486bb4245cf6e20417
+ md5: bf30bcab956adac1b10f9c8abd3b7d8b
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h9e06b3e_5_cpu
+ - libarrow-acero 24.0.0 h91633f5_5_cpu
+ - libarrow-compute 24.0.0 hb38465b_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libparquet 24.0.0 h0f82bca_5_cpu
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 534330
+ timestamp: 1781072712273
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-ha4f4840_5_cpu.conda
+ build_number: 5
+ sha256: 91f710702c3d0c5b1a42dd03fb73e3a6304be6002e5f13ee8bdf3e7104edaf5e
+ md5: 899673e361a9426cdee5da86cda0cbed
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h91214ac_5_cpu
+ - libarrow-acero 24.0.0 ha4f4840_5_cpu
+ - libarrow-compute 24.0.0 h8d10c55_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libparquet 24.0.0 h840b369_5_cpu
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 518863
+ timestamp: 1781072345132
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-hb4dd7c2_5_cpu.conda
+ build_number: 5
+ sha256: 25fe77e28b0e0cb1f11052e09527854a51aa214e94c8ab9c900a221d27e3c139
+ md5: 712928bbdab0db3daa909d880a467565
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h8ff9baf_5_cpu
+ - libarrow-acero 24.0.0 h635bf11_5_cpu
+ - libarrow-dataset 24.0.0 h635bf11_5_cpu
+ - libgcc >=14
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libstdcxx >=14
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 501578
+ timestamp: 1781069908655
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-24.0.0-h613493e_5_cpu.conda
+ build_number: 5
+ sha256: 8d88c6e43e256d8bbaca3d1f54b925cf2f71d960d68d834b534a14da0254c93e
+ md5: 97822771bac27cdbcbbd440b7df21fab
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h9e06b3e_5_cpu
+ - libarrow-acero 24.0.0 h91633f5_5_cpu
+ - libarrow-dataset 24.0.0 h91633f5_5_cpu
+ - libcxx >=21
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 448698
+ timestamp: 1781072796877
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h05be00f_5_cpu.conda
+ build_number: 5
+ sha256: acc4750d71c4551b627386ee421407a3dc84440070651949e9e88bba130856b6
+ md5: 2168b56c5615b338e6ce5787e223d333
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h91214ac_5_cpu
+ - libarrow-acero 24.0.0 ha4f4840_5_cpu
+ - libarrow-dataset 24.0.0 ha4f4840_5_cpu
+ - libcxx >=21
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 454306
+ timestamp: 1781072440866
- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-7_h4a7cf45_openblas.conda
build_number: 7
sha256: 081c850f99bc355821fac9c6e3727d40b3f8ce3beb50a5437cf03726b611ff39
@@ -8004,6 +10313,37 @@ packages:
purls: []
size: 12827971
timestamp: 1778479852868
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2
+ sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5
+ md5: c965a5aa0d5c1c37ffc62dff36e28400
+ depends:
+ - libgcc-ng >=9.4.0
+ - libstdcxx-ng >=9.4.0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 20440
+ timestamp: 1633683576494
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2
+ sha256: 3043869ac1ee84554f177695e92f2f3c2c507b260edad38a0bf3981fce1632ff
+ md5: 23d6d5a69918a438355d7cbc4c3d54c9
+ depends:
+ - libcxx >=11.1.0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 20128
+ timestamp: 1633683906221
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2
+ sha256: 58477b67cc719060b5b069ba57161e20ba69b8695d154a719cb4b60caf577929
+ md5: 32bd82a6a625ea6ce090a81c3d34edeb
+ depends:
+ - libcxx >=11.1.0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 18765
+ timestamp: 1633683992603
- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda
sha256: 205c4f19550f3647832ec44e35e6d93c8c206782bdd620c1d7cf66237580ff9c
md5: 49c553b47ff679a6a1e9fc80b9c5a2d4
@@ -8018,6 +10358,55 @@ packages:
purls: []
size: 4518030
timestamp: 1770902209173
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda
+ sha256: 75963a5dd913311f59a35dbd307592f4fa754c4808aff9c33edb430c415e38eb
+ md5: c3cc2864f82a944bc90a7beb4d3b0e88
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - krb5 >=1.22.2,<1.23.0a0
+ - libgcc >=14
+ - libnghttp2 >=1.68.1,<2.0a0
+ - libssh2 >=1.11.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ license: curl
+ license_family: MIT
+ purls: []
+ size: 468706
+ timestamp: 1777461492876
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.20.0-h8f0b9e4_0.conda
+ sha256: 5d3d8a82ca43347e96f1d79048921f3a7c25e32514bc7feb53ed2a040dcca54d
+ md5: 4a0085ccf90dc514f0fc0909a874045e
+ depends:
+ - __osx >=11.0
+ - krb5 >=1.22.2,<1.23.0a0
+ - libnghttp2 >=1.68.1,<2.0a0
+ - libssh2 >=1.11.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ license: curl
+ license_family: MIT
+ purls: []
+ size: 419676
+ timestamp: 1777462238769
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.20.0-hd5a2499_0.conda
+ sha256: 38c0bc634b61e542776e97cfd15d5d41edd304d4e47c333004d2d622439b2381
+ md5: 2f57b7d0c6adda88957586b7afd78438
+ depends:
+ - __osx >=11.0
+ - krb5 >=1.22.2,<1.23.0a0
+ - libnghttp2 >=1.68.1,<2.0a0
+ - libssh2 >=1.11.1,<2.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ license: curl
+ license_family: MIT
+ purls: []
+ size: 400568
+ timestamp: 1777462251987
- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.5-h19cb2f5_1.conda
sha256: 8f3d495df4427d9285ae25a51d32123ca251c32abebcef020fddb8ac1f200894
md5: 56fa8b3e43d26c97da88aea4e958f616
@@ -8126,20 +10515,77 @@ packages:
- libglvnd 1.7.0 ha4b6fd6_2
license: LicenseRef-libglvnd
purls: []
- size: 44840
- timestamp: 1731330973553
-- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
- sha256: f6e7095260305dc05238062142fb8db4b940346329b5b54894a90610afa6749f
- md5: b513eb83b3137eca1192c34bf4f013a7
+ size: 44840
+ timestamp: 1731330973553
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda
+ sha256: f6e7095260305dc05238062142fb8db4b940346329b5b54894a90610afa6749f
+ md5: b513eb83b3137eca1192c34bf4f013a7
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libegl 1.7.0 ha4b6fd6_2
+ - libgl-devel 1.7.0 ha4b6fd6_2
+ - xorg-libx11
+ license: LicenseRef-libglvnd
+ purls: []
+ size: 30380
+ timestamp: 1731331017249
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda
+ sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4
+ md5: 172bf1cd1ff8629f2b1179945ed45055
+ depends:
+ - libgcc-ng >=12
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 112766
+ timestamp: 1702146165126
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda
+ sha256: 0d238488564a7992942aa165ff994eca540f687753b4f0998b29b4e4d030ff43
+ md5: 899db79329439820b7e8f8de41bca902
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 106663
+ timestamp: 1702146352558
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda
+ sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f
+ md5: 36d33e440c31857372a72137f78bacf5
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 107458
+ timestamp: 1702146414478
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda
+ sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131
+ md5: a1cfcc585f0c42bf8d5546bb1dfb668d
+ depends:
+ - libgcc-ng >=12
+ - openssl >=3.1.1,<4.0a0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 427426
+ timestamp: 1685725977222
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda
+ sha256: e0bd9af2a29f8dd74309c0ae4f17a7c2b8c4b89f875ff1d6540c941eefbd07fb
+ md5: e38e467e577bd193a7d5de7c2c540b04
+ depends:
+ - openssl >=3.1.1,<4.0a0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 372661
+ timestamp: 1685726378869
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda
+ sha256: 8c136d7586259bb5c0d2b913aaadc5b9737787ae4f40e3ad1beaf96c80b919b7
+ md5: 1a109764bff3bdc7bdd84088347d71dc
depends:
- - __glibc >=2.17,<3.0.a0
- - libegl 1.7.0 ha4b6fd6_2
- - libgl-devel 1.7.0 ha4b6fd6_2
- - xorg-libx11
- license: LicenseRef-libglvnd
+ - openssl >=3.1.1,<4.0a0
+ license: BSD-3-Clause
+ license_family: BSD
purls: []
- size: 30380
- timestamp: 1731331017249
+ size: 368167
+ timestamp: 1685726248899
- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda
sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12
md5: a3b390520c563d78cc58974de95a03e5
@@ -8624,6 +11070,119 @@ packages:
purls: []
size: 603817
timestamp: 1778268942614
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.5.0-h8d2ee43_1.conda
+ sha256: 42c8ca362013d0378ba58afb61940d23c94e0f7127004190dcd12fe4a3072953
+ md5: 8ae0593085ca8148fdbf0bc8f62e79c1
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libgcc >=14
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libstdcxx >=14
+ - openssl >=3.5.6,<4.0a0
+ constrains:
+ - libgoogle-cloud 3.5.0 *_1
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 2647694
+ timestamp: 1780029060448
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-3.5.0-h8b848e0_1.conda
+ sha256: f6f23551b2f4b9c9b3e0c72398e4995702e832ee03b717e4d9802ce695f6938a
+ md5: 323f0d14ccec33e69a6c16a11f3ec7c1
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libcxx >=19
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - openssl >=3.5.6,<4.0a0
+ constrains:
+ - libgoogle-cloud 3.5.0 *_1
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 1882201
+ timestamp: 1780030929238
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.5.0-h688a705_1.conda
+ sha256: 20235ded7b8d125461a9ed5e02f174eae89e85a271d3343167015f779ebc4714
+ md5: 3899a5a69da373a85e7f53be3d32b814
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libcxx >=19
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - openssl >=3.5.6,<4.0a0
+ constrains:
+ - libgoogle-cloud 3.5.0 *_1
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 1812401
+ timestamp: 1780031033935
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.5.0-hdbdcf42_1.conda
+ sha256: 6914f9b0f2d5bb0c5687b880c6c352a2333449d03ce80e6826230675062b57f1
+ md5: 6f79d5f72cfcdd3509112233a8aedc2e
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libabseil
+ - libcrc32c >=1.1.2,<1.2.0a0
+ - libcurl
+ - libgcc >=14
+ - libgoogle-cloud 3.5.0 h8d2ee43_1
+ - libstdcxx >=14
+ - libzlib >=1.3.2,<2.0a0
+ - openssl
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 779116
+ timestamp: 1780029183339
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-3.5.0-hea209c6_1.conda
+ sha256: 086374067de8b3fd6198f87f8a7879d5042e35a7816e2a570155a3590e480a0d
+ md5: 8c84b06d18a3c83c28eb89bca378daad
+ depends:
+ - __osx >=11.0
+ - libabseil
+ - libcrc32c >=1.1.2,<1.2.0a0
+ - libcurl
+ - libcxx >=19
+ - libgoogle-cloud 3.5.0 h8b848e0_1
+ - libzlib >=1.3.2,<2.0a0
+ - openssl
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 541328
+ timestamp: 1780031289207
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.5.0-ha114238_1.conda
+ sha256: 40b7074e3837fe3dcebef0e93f1f40fb995abd94787e51d231d31142e157dadd
+ md5: ecc3983f92594b3863a7e5d47d1a71ba
+ depends:
+ - __osx >=11.0
+ - libabseil
+ - libcrc32c >=1.1.2,<1.2.0a0
+ - libcurl
+ - libcxx >=19
+ - libgoogle-cloud 3.5.0 h688a705_1
+ - libzlib >=1.3.2,<2.0a0
+ - openssl
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 527597
+ timestamp: 1780031485452
- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.1-h1d1128b_0.conda
sha256: 5bb935188999fd70f67996746fd2dca85ec6204289e11695c316772e19451eb8
md5: b5fb6d6c83f63d83ef2721dca6ff7091
@@ -8865,6 +11424,55 @@ packages:
purls: []
size: 92472
timestamp: 1775825802659
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda
+ sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f
+ md5: 2a45e7f8af083626f009645a6481f12d
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - c-ares >=1.34.6,<2.0a0
+ - libev >=4.33,<4.34.0a0
+ - libev >=4.33,<5.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - libzlib >=1.3.1,<2.0a0
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 663344
+ timestamp: 1773854035739
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda
+ sha256: 899551e16aac9dfb85bfc2fd98b655f4d1b7fea45720ec04ccb93d95b4d24798
+ md5: dba4c95e2fe24adcae4b77ebf33559ae
+ depends:
+ - __osx >=11.0
+ - c-ares >=1.34.6,<2.0a0
+ - libcxx >=19
+ - libev >=4.33,<4.34.0a0
+ - libev >=4.33,<5.0a0
+ - libzlib >=1.3.1,<2.0a0
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 606749
+ timestamp: 1773854765508
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda
+ sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a
+ md5: 6ea18834adbc3b33df9bd9fb45eaf95b
+ depends:
+ - __osx >=11.0
+ - c-ares >=1.34.6,<2.0a0
+ - libcxx >=19
+ - libev >=4.33,<4.34.0a0
+ - libev >=4.33,<5.0a0
+ - libzlib >=1.3.1,<2.0a0
+ - openssl >=3.5.5,<4.0a0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 576526
+ timestamp: 1773854624224
- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda
sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5
md5: d864d34357c3b65a4b731f78c0801dc4
@@ -8941,6 +11549,144 @@ packages:
purls: []
size: 50757
timestamp: 1731330993524
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h9692893_0.conda
+ sha256: 247b99f5dd32363d7231c9c5a6ad113e0b58ad3e85d68227999b5933d5005a6d
+ md5: 2a44700a9857b49a3fe72aca643d0921
+ depends:
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp-headers 1.27.0 ha770c72_0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - nlohmann_json
+ - prometheus-cpp >=1.3.0,<1.4.0a0
+ constrains:
+ - cpp-opentelemetry-sdk =1.27.0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 943253
+ timestamp: 1778721388532
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-1.27.0-h7a0a166_0.conda
+ sha256: 5ba2acb247c3f967c72391a912bcb4fd697de27c3e5033c6e5fa83797a4d14f2
+ md5: 2b6d466bf0d5c0fba290e168eae7ac4a
+ depends:
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp-headers 1.27.0 h694c41f_0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - nlohmann_json
+ - prometheus-cpp >=1.3.0,<1.4.0a0
+ constrains:
+ - cpp-opentelemetry-sdk =1.27.0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 604491
+ timestamp: 1778721948053
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h08d5cc3_0.conda
+ sha256: db60a4d6eb5be208f8a0be686909b1f10635b3913a7c1ce391d4d26d991115c3
+ md5: 35e93c8c0edb8dff7f9ebeb55ec4e6a6
+ depends:
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcurl >=8.20.0,<9.0a0
+ - libgrpc >=1.78.1,<1.79.0a0
+ - libopentelemetry-cpp-headers 1.27.0 hce30654_0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - nlohmann_json
+ - prometheus-cpp >=1.3.0,<1.4.0a0
+ constrains:
+ - cpp-opentelemetry-sdk =1.27.0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 582427
+ timestamp: 1778721505645
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_0.conda
+ sha256: 4a55bd84d166395a117592bb6139cf645eb402416987b856b41f96ba7b9d15d6
+ md5: f8dcb0cff8f84f428bf76f1169bf50a7
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 392177
+ timestamp: 1778721367721
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libopentelemetry-cpp-headers-1.27.0-h694c41f_0.conda
+ sha256: 887e0e2f9864b3a4f2565222a07d2d6544ce16f62b2a5637211d2e022dcdf777
+ md5: 56d102b4190f3170dad25651544e6263
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 393506
+ timestamp: 1778721872019
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_0.conda
+ sha256: 64724bf5c5c48ecbc92a7d561654c6305d6dc819e0773c8989877f0613e52542
+ md5: f8039fbb88b31890de23c8a16ae03d92
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 394303
+ timestamp: 1778721455052
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_5_cpu.conda
+ build_number: 5
+ sha256: 0c627751bfc2e3fd35ada8a54e3c5b9f906a3dc0b9a1c02d9a9a488303babd8b
+ md5: e5b85d3c6fac8b8b713a93aedce28364
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libarrow 24.0.0 h8ff9baf_5_cpu
+ - libgcc >=14
+ - libstdcxx >=14
+ - libthrift >=0.22.0,<0.22.1.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 1428207
+ timestamp: 1781069771394
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-24.0.0-h0f82bca_5_cpu.conda
+ build_number: 5
+ sha256: 8c97c80b54074117d3cb2e8de32167fab7f1f14fc430f60680c9fa3168a176c0
+ md5: d53c4977b9e32579267ef6d47809a926
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h9e06b3e_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libthrift >=0.22.0,<0.22.1.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 1120282
+ timestamp: 1781072235499
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h840b369_5_cpu.conda
+ build_number: 5
+ sha256: e5337f70681a6d8aea93b36d8ef27958a04840eb83f7aa2bc7ab823ccbecde2c
+ md5: 3a10960f0a306683037cb93533e18604
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libarrow 24.0.0 h91214ac_5_cpu
+ - libcxx >=21
+ - libopentelemetry-cpp >=1.27.0,<1.28.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libthrift >=0.22.0,<0.22.1.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 1097984
+ timestamp: 1781072012333
- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda
sha256: f41721636a7c2e51bc2c642e1127955ab9c81145470714fdaac44d4d09e4af41
md5: 33082e13b4769b48cfeb648e15bfe3fc
@@ -9263,6 +12009,49 @@ packages:
purls: []
size: 27776
timestamp: 1778269074600
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda
+ sha256: af6025aa4a4fc3f4e71334000d2739d927e2f678607b109ec630cc17d716918a
+ md5: b6e326fbe1e3948da50ec29cee0380db
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libevent >=2.1.12,<2.1.13.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 423861
+ timestamp: 1777018957474
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.22.0-hebea4ca_2.conda
+ sha256: 89a20cb35e0f32d59a7080c934a56120591cb962d4fab1cba3a795a094bc8256
+ md5: 36d5479e1b5967c2eb9824b953317e41
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - libevent >=2.1.12,<2.1.13.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 332270
+ timestamp: 1777019812419
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda
+ sha256: 568bb23db02b050c3903bec05edbcab84960c8c7e5a1710dac3109df997ac7f1
+ md5: d006875f9a58a44f92aec9a7ebeb7150
+ depends:
+ - __osx >=11.0
+ - libcxx >=19
+ - libevent >=2.1.12,<2.1.13.0a0
+ - libzlib >=1.3.2,<2.0a0
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 323017
+ timestamp: 1777019893083
- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda
sha256: e5f8c38625aa6d567809733ae04bb71c161a42e44a9fa8227abe61fa5c60ebe0
md5: cd5a90476766d53e901500df9215e927
@@ -9315,6 +12104,37 @@ packages:
purls: []
size: 373892
timestamp: 1762022345545
+- conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda
+ sha256: ecbf4b7520296ed580498dc66a72508b8a79da5126e1d6dc650a7087171288f9
+ md5: 1247168fe4a0b8912e3336bccdbf98a5
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 85969
+ timestamp: 1768735071295
+- conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.11.3-hc282952_0.conda
+ sha256: 626db214208e8da6aa9a904518a0442e5bff7b4602cc295dd5ce1f4a98844c1d
+ md5: 2c49b6f6ec9a510bbb75ecbd2a572697
+ depends:
+ - __osx >=10.13
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 84535
+ timestamp: 1768735249136
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda
+ sha256: ae1a82e62cd4e3c18e005ae7ff4358ed72b2bfbfe990d5a6a5587f81e9a100dc
+ md5: 2255add2f6ae77d0a96624a5cbde6d45
+ depends:
+ - __osx >=11.0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 87916
+ timestamp: 1768735311947
- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda
sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829
md5: 38ffe67b78c9d4de527be8315e5ada2c
@@ -9754,6 +12574,40 @@ packages:
- pkg:pypi/lxml?source=hash-mapping
size: 1349054
timestamp: 1776512963897
+- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda
+ sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346
+ md5: 9de5350a85c4a20c685259b889aa6393
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=13
+ - libstdcxx >=13
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 167055
+ timestamp: 1733741040117
+- conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.10.0-h240833e_1.conda
+ sha256: 8da3c9d4b596e481750440c0250a7e18521e7f69a47e1c8415d568c847c08a1c
+ md5: d6b9bd7e356abd7e3a633d59b753495a
+ depends:
+ - __osx >=10.13
+ - libcxx >=18
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 159500
+ timestamp: 1733741074747
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda
+ sha256: 94d3e2a485dab8bdfdd4837880bde3dd0d701e2b97d6134b8806b7c8e69c8652
+ md5: 01511afc6cc1909c5303cf31be17b44f
+ depends:
+ - __osx >=11.0
+ - libcxx >=18
+ license: BSD-2-Clause
+ license_family: BSD
+ purls: []
+ size: 148824
+ timestamp: 1733741047892
- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda
sha256: d06d02574be3892020262464b49360a749c1d448ed9f0de52fe8a08bc1483261
md5: a73036dabdd6dfe9679ed893baa8b230
@@ -10369,6 +13223,36 @@ packages:
- pkg:pypi/networkx?source=hash-mapping
size: 1587439
timestamp: 1765215107045
+- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda
+ sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63
+ md5: 16c2a0e9c4a166e53632cfca4f68d020
+ constrains:
+ - nlohmann_json-abi ==3.12.0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 136216
+ timestamp: 1758194284857
+- conda: https://conda.anaconda.org/conda-forge/osx-64/nlohmann_json-3.12.0-h06076ce_1.conda
+ sha256: 8e1b8ac88e07da2910c72466a94d1fc77aa13c722f8ddbc7ae3beb7c19b41fc7
+ md5: 97d7a1cda5546cb0bbdefa3777cb9897
+ constrains:
+ - nlohmann_json-abi ==3.12.0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 137081
+ timestamp: 1768670842725
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda
+ sha256: 1945fd5b64b74ef3d57926156fb0bfe88ee637c49f3273067f7231b224f1d26d
+ md5: 755cfa6c08ed7b7acbee20ccbf15a47c
+ constrains:
+ - nlohmann_json-abi ==3.12.0
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 137595
+ timestamp: 1768670878127
- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.7-pyhcf101f3_1.conda
sha256: f4974b9984376f75fe8cabab06b25e23addaa7d962e8a713da3f4a213ded8c7b
md5: 92b46e8c96039d3d7c4958f426bca753
@@ -10770,15 +13654,73 @@ packages:
sha256: 90afae35aede3e3d779976ca9baa7af192bd21e9365ba51b1962933796f49b79
md5: 1ccdb985da56ab7f200856642163bf3a
depends:
- - bottle >=0.13.0
- - optuna >=3.6.0
- - python >=3.10
- license: MIT
- license_family: MIT
- purls:
- - pkg:pypi/optuna-dashboard?source=hash-mapping
- size: 6659356
- timestamp: 1762756075150
+ - bottle >=0.13.0
+ - optuna >=3.6.0
+ - python >=3.10
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/optuna-dashboard?source=hash-mapping
+ size: 6659356
+ timestamp: 1762756075150
+- conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda
+ sha256: a60c2578c8422e0c67206d269767feb4d3e627511558b6866e5daf2231d5214d
+ md5: 8027fce94fdfdf2e54f9d18cbae496df
+ depends:
+ - tzdata
+ - libstdcxx >=14
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - lz4-c >=1.10.0,<1.11.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - libabseil >=20260107.1,<20260108.0a0
+ - libabseil * cxx17*
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ - libzlib >=1.3.1,<2.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 1468651
+ timestamp: 1773230208923
+- conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.3.0-hb9b210e_0.conda
+ sha256: c4872822be78b2503bba06b906604c87000e3a63c7b7b8cb459463d46c55814b
+ md5: 292d30447800bc51a0d3e0e9738f5730
+ depends:
+ - tzdata
+ - libcxx >=19
+ - __osx >=11.0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libzlib >=1.3.1,<2.0a0
+ - zstd >=1.5.7,<1.6.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - lz4-c >=1.10.0,<1.11.0a0
+ - libabseil >=20260107.1,<20260108.0a0
+ - libabseil * cxx17*
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 594601
+ timestamp: 1773230256637
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hd11884d_0.conda
+ sha256: 8594f064828cca9b8d625e2ebe79436fd4ffc030c950573380c54a8f4329f955
+ md5: 77bfe521901c1a247cc66c1276826a85
+ depends:
+ - tzdata
+ - libcxx >=19
+ - __osx >=11.0
+ - zstd >=1.5.7,<1.6.0a0
+ - libzlib >=1.3.1,<2.0a0
+ - snappy >=1.2.2,<1.3.0a0
+ - libprotobuf >=6.33.5,<6.33.6.0a0
+ - libabseil >=20260107.1,<20260108.0a0
+ - libabseil * cxx17*
+ - lz4-c >=1.10.0,<1.11.0a0
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 548180
+ timestamp: 1773230270828
- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda
sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c
md5: e51f1e4089cad105b6cac64bd8166587
@@ -11323,6 +14265,49 @@ packages:
- pkg:pypi/ppft?source=hash-mapping
size: 35537
timestamp: 1769007436649
+- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
+ sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc
+ md5: a83f6a2fdc079e643237887a37460668
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libcurl >=8.10.1,<9.0a0
+ - libgcc >=13
+ - libstdcxx >=13
+ - libzlib >=1.3.1,<2.0a0
+ - zlib
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 199544
+ timestamp: 1730769112346
+- conda: https://conda.anaconda.org/conda-forge/osx-64/prometheus-cpp-1.3.0-h7802330_0.conda
+ sha256: af754a477ee2681cb7d5d77c621bd590d25fe1caf16741841fc2d176815fc7de
+ md5: f36107fa2557e63421a46676371c4226
+ depends:
+ - __osx >=10.13
+ - libcurl >=8.10.1,<9.0a0
+ - libcxx >=18
+ - libzlib >=1.3.1,<2.0a0
+ - zlib
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 179103
+ timestamp: 1730769223221
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda
+ sha256: 851a77ae1a8e90db9b9f3c4466abea7afb52713c3d98ceb0d37ba6ff27df2eff
+ md5: 7172339b49c94275ba42fec3eaeda34f
+ depends:
+ - __osx >=11.0
+ - libcurl >=8.10.1,<9.0a0
+ - libcxx >=18
+ - libzlib >=1.3.1,<2.0a0
+ - zlib
+ license: MIT
+ license_family: MIT
+ purls: []
+ size: 173220
+ timestamp: 1730769371051
- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda
sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c
md5: a11ab1f31af799dd93c3a39881528884
@@ -11358,6 +14343,64 @@ packages:
purls: []
size: 7212
timestamp: 1756321849562
+- conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda
+ sha256: 53997629b27a2465989f23e3a6212cfcc19f51c474d8f89905bb99859140ee88
+ md5: 0aee4f9ff95fc4bc78264a93e2a74adf
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libgcc >=14
+ - libstdcxx >=14
+ - libzlib >=1.3.1,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - libprotobuf 6.33.5
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/protobuf?source=hash-mapping
+ size: 481654
+ timestamp: 1773265949321
+- conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.33.5-py312hf5f8d9f_2.conda
+ sha256: e0cf97e208c236656576c8eace665c756e0e1f840b6191169f22b758d63633e8
+ md5: fcf0fc2b6f794dc454a5521542b97e90
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcxx >=19
+ - libzlib >=1.3.1,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - libprotobuf 6.33.5
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/protobuf?source=hash-mapping
+ size: 469618
+ timestamp: 1773265967461
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.33.5-py312h857ab9a_2.conda
+ sha256: 3e8720f23829f60103f61fc5e53ac36535da5896855a98d2bbbc6a35e78be430
+ md5: db3dd88ca6652ab2485d11f88a2b1d59
+ depends:
+ - __osx >=11.0
+ - libabseil * cxx17*
+ - libabseil >=20260107.1,<20260108.0a0
+ - libcxx >=19
+ - libzlib >=1.3.1,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - libprotobuf 6.33.5
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/protobuf?source=hash-mapping
+ size: 465012
+ timestamp: 1773265421534
- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h5253ce2_0.conda
sha256: d834fd656133c9e4eaf63ffe9a117c7d0917d86d89f7d64073f4e3a0020bd8a7
md5: dd94c506b119130aef5a9382aed648e7
@@ -11466,6 +14509,116 @@ packages:
- scipy ; extra == 'scipy'
- wand ; extra == 'wand'
- parakimo ; extra == 'parakimo'
+- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py312h7900ff3_0.conda
+ sha256: ce11f466f0dcfc41bd0ef3719adb396fbffa6b866aac75c9242938fa58e546d5
+ md5: f9ced0be11f0d3120336e4171a121c2a
+ depends:
+ - libarrow-acero 24.0.0.*
+ - libarrow-dataset 24.0.0.*
+ - libarrow-substrait 24.0.0.*
+ - libparquet 24.0.0.*
+ - pyarrow-core 24.0.0 *_0_*
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 26787
+ timestamp: 1776927989772
+- conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-24.0.0-py312hb401068_0.conda
+ sha256: 9f34bf129e8a618b6a8fecdfafa509823695b945d2618205758fab529ee9b88f
+ md5: 3301b7a88750f114aa09fc2a28db2435
+ depends:
+ - libarrow-acero 24.0.0.*
+ - libarrow-dataset 24.0.0.*
+ - libarrow-substrait 24.0.0.*
+ - libparquet 24.0.0.*
+ - pyarrow-core 24.0.0 *_0_*
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 26749
+ timestamp: 1776929273540
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py312h1f38498_0.conda
+ sha256: c7cc2c75525c6522f9b948cd9ead2d5ceec55ba8b78cfd6f222fbc581219d0ff
+ md5: 2b3892c12915851e12955ee753eaedbb
+ depends:
+ - libarrow-acero 24.0.0.*
+ - libarrow-dataset 24.0.0.*
+ - libarrow-substrait 24.0.0.*
+ - libparquet 24.0.0.*
+ - pyarrow-core 24.0.0 *_0_*
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ license: Apache-2.0
+ license_family: APACHE
+ purls: []
+ size: 26799
+ timestamp: 1776928498495
+- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py312h2054cf2_0_cpu.conda
+ sha256: cb2c89aeb97a97572869200e37c153098c5877c7be4774f045459976e0f70233
+ md5: fe5033add07e3cf4876fd091b5fecf31
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libarrow 24.0.0.* *cpu
+ - libarrow-compute 24.0.0.* *cpu
+ - libgcc >=14
+ - libstdcxx >=14
+ - libzlib >=1.3.2,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - numpy >=1.23,<3
+ - apache-arrow-proc * cpu
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/pyarrow?source=hash-mapping
+ size: 5401544
+ timestamp: 1776927982900
+- conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-24.0.0-py312h3987635_0_cpu.conda
+ sha256: dddbcf6e50454794ebd8ba77e892a099dbee4ad9727562e140f765ce0c08552d
+ md5: 40fe539ada22b325955f3590aefe39a9
+ depends:
+ - __osx >=11.0
+ - libarrow 24.0.0.* *cpu
+ - libarrow-compute 24.0.0.* *cpu
+ - libcxx >=21
+ - libzlib >=1.3.2,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - numpy >=1.23,<3
+ - apache-arrow-proc * cpu
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/pyarrow?source=hash-mapping
+ size: 4055527
+ timestamp: 1776929225059
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py312h21b41d0_0_cpu.conda
+ sha256: 38049b8b098fa02446e97bedebdde2ff4cae4b579581a7125da3e751bcf5a54d
+ md5: 7020684cfd5c066e86cee8affad06e83
+ depends:
+ - __osx >=11.0
+ - libarrow 24.0.0.* *cpu
+ - libarrow-compute 24.0.0.* *cpu
+ - libcxx >=21
+ - libzlib >=1.3.2,<2.0a0
+ - python >=3.12,<3.13.0a0
+ - python >=3.12,<3.13.0a0 *_cpython
+ - python_abi 3.12.* *_cp312
+ constrains:
+ - apache-arrow-proc * cpu
+ - numpy >=1.23,<3
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/pyarrow?source=hash-mapping
+ size: 4322018
+ timestamp: 1776928464897
- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda
sha256: e27e0473fc6723311a0bd48b89b616fa1b996a2f7a2b555338cbbcfb9c640568
md5: 9c5491066224083c41b6d5635ed7107b
@@ -11478,6 +14631,23 @@ packages:
- pkg:pypi/pycparser?source=compressed-mapping
size: 55886
timestamp: 1779293633166
+- conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.2-pyhd8ed1ab_0.conda
+ sha256: 25a4fb0acf8a1eb3eb0f1ebc1c5610dd9efe5f29616c625d1ee84a9a76eeb5df
+ md5: 18301be1ab749bc57ae189377836cc47
+ depends:
+ - jinja2 >=2.10.1
+ - numpy >=1.16.4
+ - python >=3.10
+ constrains:
+ - ipywidgets >=7,<8
+ - traitlets >=4.3.2
+ - ipykernel >=5.1.2
+ license: Apache-2.0
+ license_family: Apache
+ purls:
+ - pkg:pypi/pydeck?source=hash-mapping
+ size: 8360396
+ timestamp: 1776371116205
- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda
sha256: af7213a8ca077895e7e10c8f33d5de3436b8a26828422e8a113cc59c9277a3e2
md5: 15f6d0866b0997c5302fc230a566bc72
@@ -11951,6 +15121,18 @@ packages:
- pkg:pypi/python-json-logger?source=hash-mapping
size: 18969
timestamp: 1777318679482
+- conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda
+ sha256: 7dde902994a99993b616d597783fcd0ce3265a550feffbd0890b1329c9dff7e0
+ md5: f54d00b369042e8e0e235b1a1a817487
+ depends:
+ - python >=3.10
+ - python
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/python-multipart?source=compressed-mapping
+ size: 38132
+ timestamp: 1780610429919
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9
md5: f6ad7450fc21e00ecc23812baed6d2e4
@@ -12378,6 +15560,18 @@ packages:
- pkg:pypi/rpds-py?source=compressed-mapping
size: 294437
timestamp: 1779977098659
+- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.3-hc5a330e_0.conda
+ sha256: 150a0a5254e8b15ad737549721c7d13406cd96432f3f446e07073dbd98bb2491
+ md5: f2bd09e21c5844a12e2f5eefcd075555
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - openssl >=3.5.6,<4.0a0
+ license: Apache-2.0
+ license_family: Apache
+ purls: []
+ size: 388111
+ timestamp: 1778113913631
- conda: https://conda.anaconda.org/conda-forge/noarch/salib-1.5.2-pyhd8ed1ab_0.conda
sha256: fc971e80731e39b4e09d05430ccb434cf72cb6a98dd6fcd7db806f59d5e87c6e
md5: 2b46f618a11ce94c030d371707350991
@@ -12615,6 +15809,53 @@ packages:
- pkg:pypi/six?source=hash-mapping
size: 18455
timestamp: 1753199211006
+- conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda
+ sha256: bc86861086db65c9b56dd6a1605755f41a9875b94fc3b69e137f686700d91aa1
+ md5: b899f1195be56d8215ed1973a8f409c3
+ depends:
+ - python >=3.10
+ - python
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/smmap?source=compressed-mapping
+ size: 27262
+ timestamp: 1781021238494
+- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda
+ sha256: 48f3f6a76c34b2cfe80de9ce7f2283ecb55d5ed47367ba91e8bb8104e12b8f11
+ md5: 98b6c9dc80eb87b2519b97bcf7e578dd
+ depends:
+ - libgcc >=14
+ - __glibc >=2.17,<3.0.a0
+ - libstdcxx >=14
+ - libgcc >=14
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 45829
+ timestamp: 1762948049098
+- conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda
+ sha256: 1525e6d8e2edf32dabfe2a8e2fc8bf2df81c5ef9f0b5374a3d4ccfa672bfd949
+ md5: 2e993292ec18af5cd480932d448598cf
+ depends:
+ - libcxx >=19
+ - __osx >=10.13
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 40023
+ timestamp: 1762948053450
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda
+ sha256: cb9305ede19584115f43baecdf09a3866bfcd5bcca0d9e527bd76d9a1dbe2d8d
+ md5: fca4a2222994acd7f691e57f94b750c5
+ depends:
+ - libcxx >=19
+ - __osx >=11.0
+ license: BSD-3-Clause
+ license_family: BSD
+ purls: []
+ size: 38883
+ timestamp: 1762948066818
- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda
sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad
md5: 03fe290994c5e4ec17293cfb6bdce520
@@ -12698,6 +15939,19 @@ packages:
- pkg:pypi/stack-data?source=hash-mapping
size: 26988
timestamp: 1733569565672
+- conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.3.0-pyhcf101f3_0.conda
+ sha256: 4db81efb63dd859e695a09f279a42d96173a4bcbdbc553390917666a4769e104
+ md5: cc5611d8c1b50aeaac99848acec6612e
+ depends:
+ - anyio >=3.6.2,<5
+ - python >=3.10
+ - typing_extensions >=4.10.0
+ - python
+ license: BSD-3-Clause
+ purls:
+ - pkg:pypi/starlette?source=compressed-mapping
+ size: 64167
+ timestamp: 1781176104669
- conda: https://conda.anaconda.org/conda-forge/linux-64/statsmodels-0.14.6-py312h4f23490_0.conda
sha256: 0c61eccf3f71b9812da8ced747b1f22bafd6f66f9a64abe06bbe147a03b7322e
md5: 423b8676bd6eed60e97097b33f13ea3f
@@ -12757,6 +16011,54 @@ packages:
- pkg:pypi/statsmodels?source=hash-mapping
size: 11537488
timestamp: 1764984166760
+- conda: https://conda.anaconda.org/conda-forge/noarch/streamlit-1.58.0-pyhd8ed1ab_0.conda
+ sha256: f7a65e5dfd04160d44e9d1410f75f69eee16c82843abd56c3a71e31e6664dde3
+ md5: a1e5b0b9f0097b9411cbf477eec37f7c
+ depends:
+ - altair >=4.0,<7,!=5.4.0,!=5.4.1
+ - anyio >=4.0.0
+ - blinker >=1.5.0,<2
+ - cachetools >=5.5,<8
+ - click >=7.0,<9
+ - gitpython >=3.0.7,<4,!=3.1.19
+ - httptools >=0.6.3
+ - itsdangerous >=2.1.2
+ - numpy >=1.23,<3
+ - packaging >=20
+ - pandas >=1.4.0,<4
+ - pillow >=7.1.0,<13
+ - protobuf >=3.20,<8
+ - pyarrow >=7.0
+ - pydeck >=0.8.0b4,<1
+ - python >=3.10
+ - python-multipart >=0.0.10
+ - requests >=2.27,<3
+ - starlette >=0.40.0
+ - tenacity >=8.1.0,<10
+ - toml >=0.10.1,<2
+ - tornado >=6.0.3,<7,!=6.5.0
+ - typing_extensions >=4.10.0,<5
+ - uvicorn >=0.30.0
+ - watchdog >=2.1.5,<7
+ - websockets >=12.0.0
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/streamlit?source=compressed-mapping
+ size: 7387864
+ timestamp: 1780060644912
+- conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda
+ sha256: 32e75900d6a094ffe4290a8c9f1fa15744d9da8ff617aba4acaa0f057a065c34
+ md5: 043f0599dc8aa023369deacdb5ac24eb
+ depends:
+ - python >=3.10
+ - python
+ license: Apache-2.0
+ license_family: APACHE
+ purls:
+ - pkg:pypi/tenacity?source=hash-mapping
+ size: 31404
+ timestamp: 1770510172846
- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda
sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb
md5: 17b43cee5cc84969529d5d0b0309b2cb
@@ -12835,6 +16137,18 @@ packages:
purls: []
size: 3127137
timestamp: 1769460817696
+- conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhcf101f3_3.conda
+ sha256: fd30e43699cb22ab32ff3134d3acf12d6010b5bbaa63293c37076b50009b91f8
+ md5: d0fc809fa4c4d85e959ce4ab6e1de800
+ depends:
+ - python >=3.10
+ - python
+ license: MIT
+ license_family: MIT
+ purls:
+ - pkg:pypi/toml?source=hash-mapping
+ size: 24017
+ timestamp: 1764486833072
- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda
sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd
md5: b5325cf06a000c5b14970462ff5e4d58
@@ -13035,6 +16349,22 @@ packages:
- pkg:pypi/urllib3?source=hash-mapping
size: 103560
timestamp: 1778188657149
+- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.49.0-pyhc90fa1f_0.conda
+ sha256: dc7477d200432bf07aab5218d305d88771c52fb8d449cf82869cdceae291f100
+ md5: 29a950390b7de7577d8c0ebc946055b9
+ depends:
+ - __unix
+ - click >=7.0
+ - h11 >=0.8
+ - python >=3.10
+ - typing_extensions >=4.0
+ - python
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/uvicorn?source=compressed-mapping
+ size: 56228
+ timestamp: 1780574319325
- conda: https://conda.anaconda.org/conda-forge/noarch/vulture-2.16-pyhd8ed1ab_0.conda
sha256: 8110ce046263dfaf60b79f81e6b58189352ce0332dc9410e3e0f5dd05a6c17f0
md5: 07eb801541c33aef793e6ce25de33eab
@@ -13158,6 +16488,47 @@ packages:
- pkg:pypi/websocket-client?source=hash-mapping
size: 61391
timestamp: 1759928175142
+- conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-16.0-py312h5253ce2_1.conda
+ sha256: dd598cab9175a9ab11c8a1798c49ccabe923263d12aababa84a296cb18206464
+ md5: e35ffb48178b20ee1a43fbe7abc93746
+ depends:
+ - python
+ - __glibc >=2.17,<3.0.a0
+ - libgcc >=14
+ - python_abi 3.12.* *_cp312
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/websockets?source=hash-mapping
+ size: 358659
+ timestamp: 1768087389177
+- conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-16.0-py312hf7082af_1.conda
+ sha256: f829712bdea8354b2f8a839f424c30a9105f244224539eb70a0bdbbe3daf66ea
+ md5: 87a96153ad92bee0cac8461ce243fa83
+ depends:
+ - python
+ - __osx >=10.13
+ - python_abi 3.12.* *_cp312
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/websockets?source=hash-mapping
+ size: 359204
+ timestamp: 1768087387150
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-16.0-py312hb3ab3e3_1.conda
+ sha256: 4b15497f3cbc40c6fc9e0f155e9cd31aa13e8d2cb1930355da934af22816a73a
+ md5: 3da07548ed0e08634abf2b3b878eabc1
+ depends:
+ - python
+ - python 3.12.* *_cpython
+ - __osx >=11.0
+ - python_abi 3.12.* *_cp312
+ license: BSD-3-Clause
+ license_family: BSD
+ purls:
+ - pkg:pypi/websockets?source=hash-mapping
+ size: 362390
+ timestamp: 1768087403337
- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.47.0-pyhd8ed1ab_0.conda
sha256: 9e156ffaefb8463437144326ada4b85d1de17961b9997ac5f1cbbaf747bd8bed
md5: d0e3b2f0030cf4fca58bde71d246e94c
@@ -13170,17 +16541,18 @@ packages:
- pkg:pypi/wheel?source=hash-mapping
size: 33491
timestamp: 1776878563806
-- conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda
- sha256: 826af5e2c09e5e45361fa19168f46ff524e7a766022615678c3a670c45895d9a
- md5: dc257b7e7cad9b79c1dfba194e92297b
+- conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-3.6.10-pyhd8ed1ab_0.conda
+ sha256: 6aeb16d2aacdae68ba7afd980925264f5d0459dd165e3406f13f23949df346c1
+ md5: 4d52bbdb661dc1b5a1c2aeb1afcd9a67
depends:
- - python >=3.10
+ - notebook >=4.4.1
+ - python >=3.7
license: BSD-3-Clause
license_family: BSD
purls:
- pkg:pypi/widgetsnbextension?source=hash-mapping
- size: 889195
- timestamp: 1762040404362
+ size: 633673
+ timestamp: 1729587712065
- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.1.2-py312h4c3975b_0.conda
sha256: 5bf21e14a364018a36869a16d9f706fb662c6cb6da3066100ba6822a70f93d2d
md5: 7f2ef073d94036f8b16b6ee7d3562a88
@@ -13697,6 +17069,39 @@ packages:
- pkg:pypi/zipp?source=hash-mapping
size: 24461
timestamp: 1776131454755
+- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda
+ sha256: 245c9ee8d688e23661b95e3c6dd7272ca936fabc03d423cdb3cdee1bbcf9f2f2
+ md5: c2a01a08fc991620a74b32420e97868a
+ depends:
+ - __glibc >=2.17,<3.0.a0
+ - libzlib 1.3.2 h25fd6f3_2
+ license: Zlib
+ license_family: Other
+ purls: []
+ size: 95931
+ timestamp: 1774072620848
+- conda: https://conda.anaconda.org/conda-forge/osx-64/zlib-1.3.2-hbb4bfdb_2.conda
+ sha256: 5dd728cebca2e96fa48d41661f1a35ed0ee3cb722669eee4e2d854c6745655eb
+ md5: 6276aa61ffc361cbf130d78cfb88a237
+ depends:
+ - __osx >=11.0
+ - libzlib 1.3.2 hbb4bfdb_2
+ license: Zlib
+ license_family: Other
+ purls: []
+ size: 92411
+ timestamp: 1774073075482
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda
+ sha256: 8dd2ac25f0ba714263aac5832d46985648f4bfb9b305b5021d702079badc08d2
+ md5: f1c0bce276210bed45a04949cfe8dc20
+ depends:
+ - __osx >=11.0
+ - libzlib 1.3.2 h8088a28_2
+ license: Zlib
+ license_family: Other
+ purls: []
+ size: 81123
+ timestamp: 1774072974535
- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda
sha256: ea4e50c465d70236408cb0bfe0115609fd14db1adcd8bd30d8918e0291f8a75f
md5: 2aadb0d17215603a82a2a6b0afd9a4cb
diff --git a/pixi.toml b/pixi.toml
index d64811a..a3ad050 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -1,6 +1,6 @@
[workspace]
name = "phoskintime"
-version = "0.4.0"
+version = "0.5.0"
description = "PhosKinTime: ODE-based modeling of phosphorylation kinetics and transcriptional time-series."
authors = ["Abhinav Mishra "]
channels = ["conda-forge", "bioconda"]
@@ -26,6 +26,7 @@ plotly = "*"
openpyxl = "*"
xlsxwriter = "*"
adjusttext = "*"
+ansi2html = "*"
# Optimization, sensitivity analysis, and modeling utilities
pymoo = "*"
@@ -42,6 +43,8 @@ graphviz = "*"
python-graphviz = "*"
pydot = "*"
networkx = "*"
+gravis = "*"
+imageio = "*"
# Bioinformatics / annotation utilities
mygene = "*"
@@ -50,6 +53,7 @@ mygene = "*"
tqdm = "*"
python-dotenv = "*"
typer = "*"
+streamlit = "*"
jinja2 = "*"
tomli = "*"
git-cliff = ">=2.13.1,<3"
@@ -128,6 +132,8 @@ phoskintime = "PYTHONPATH=.:.. python -m phoskintime.config.cli"
phoskintime-all = "PYTHONPATH=.:.. python -m phoskintime.config.cli all"
network-dashboard = "PYTHONPATH=. python run_dashboard.py"
+dashboard = "PYTHONPATH=. streamlit run dashboard/app.py"
+dashboard-dev = "PYTHONPATH=. streamlit run dashboard/app.py --server.runOnSave=true"
docs-serve = "zensical serve"
docs-build = "zensical build"
diff --git a/protwise/runner/main.py b/protwise/runner/main.py
index cc94c0b..f168a3d 100644
--- a/protwise/runner/main.py
+++ b/protwise/runner/main.py
@@ -5,41 +5,34 @@
os.environ.setdefault("VECLIB_MAXIMUM_THREADS", "1")
os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
+import argparse
import atexit
import logging
-import pandas as pd
-from config.helpers import location
-from config.config import parse_args, extract_config, log_config
-from config.constants import (model_type, OUT_DIR, TIME_POINTS, OUT_RESULTS_DIR, DEV_TEST,
- NUM_TRAJECTORIES, PARAMETER_SPACE, PERTURBATIONS_VALUE, ALPHA_CI,
- SENSITIVITY_ANALYSIS, USE_REGULARIZATION, Y_METRIC, Y_METRIC_DESCRIPTIONS,
- DELTA_WEIGHT, ALPHA_WEIGHT, BETA_WEIGHT, GAMMA_WEIGHT, MU_WEIGHT)
-from config.logconf import setup_logger
-from protwise.paramest.core import process_gene_wrapper
-from protwise.plotting import Plotter
-from common.utils import latexit
-from common.utils.display import ensure_output_directory, save_result, organize_output_files, create_report, merge_obs_est
-
-logger = setup_logger()
-
-# Check if OUT_DIR, TIME_POINTS, OUT_RESULTS_DIR, ESTIMATION_MODE are defined
-if OUT_DIR is None or TIME_POINTS is None or OUT_RESULTS_DIR is None:
- logger.error("Output directory, time points, not defined. Exiting.")
- exit(1)
-
-# Parse command line arguments and extract configuration
-args = parse_args()
-
-# Check if the arguments are valid
-if not args:
- logger.error("Invalid arguments. Exiting.")
- exit(1)
-config = extract_config(args)
-
-# Check if the configuration is valid
-if not config:
- logger.error("Invalid configuration. Exiting.")
- exit(1)
+import sys
+from pathlib import Path
+
+ODE_CONFIG_ENV = "PHOSKINTIME_ODE_CONFIG"
+
+
+def _parse_config_path(argv: list[str] | None = None) -> Path | None:
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument("--conf", default=None)
+ known, _ = parser.parse_known_args(argv)
+ return Path(known.conf).expanduser().resolve() if known.conf else None
+
+
+def _select_config_for_runtime(argv: list[str] | None = None) -> None:
+ conf_path = _parse_config_path(argv)
+ if conf_path is not None:
+ os.environ[ODE_CONFIG_ENV] = str(conf_path)
+ for module_name in list(sys.modules):
+ if module_name in {"config.constants", "config.config"} or module_name.startswith((
+ "protwise.paramest",
+ "protwise.plotting",
+ "protwise.models",
+ )):
+ sys.modules.pop(module_name, None)
+
@atexit.register
def _close_log_handlers():
@@ -51,25 +44,94 @@ def _close_log_handlers():
except Exception:
pass
-def main():
+
+def _metadata_extra(config: dict) -> dict:
+ return {
+ "supplied_config_path": config.get("supplied_config_path"),
+ "resolved_config_path": config.get("resolved_config_path"),
+ "config_source": config.get("config_source"),
+ "effective_inputs": {
+ "protein": config.get("input_excel_protein"),
+ "phosphosite": config.get("input_excel_psite"),
+ "rna": config.get("input_excel_rna"),
+ },
+ "effective_bounds": config.get("bounds"),
+ "effective_bootstraps": config.get("bootstraps"),
+ "effective_time_grid": config.get("time_points"),
+ "output_directory": config.get("outdir"),
+ }
+
+
+
+def initialize_run_contract(config: dict, args, logger):
+ from common.results import (
+ attach_file_console_logger,
+ ensure_result_dir,
+ write_command,
+ write_metadata,
+ write_resolved_config,
+ )
+
+ out_dir = ensure_result_dir(config["outdir"])["root"]
+ out_results_dir = out_dir / Path(config["out_results_dir"]).name
+ config["outdir"] = str(out_dir)
+ config["out_results_dir"] = str(out_results_dir)
+ attach_file_console_logger(logger, out_dir)
+ write_command(out_dir)
+ write_resolved_config(out_dir, config)
+ write_metadata(
+ out_dir,
+ workflow="protwise.runner",
+ args=args,
+ inputs=[config.get("input_excel_protein"), config.get("input_excel_psite"), config.get("input_excel_rna")],
+ extra=_metadata_extra(config),
+ )
+ return out_dir, out_results_dir
+
+def main(argv: list[str] | None = None):
"""
Main function to run the phosphorylation modelling process.
- It reads the configuration, loads the data, and processes each gene in parallel.
- It also handles logging and output organization.
-
- Args:
- None
- Returns:
- None
+ It reads the selected configuration, loads the data, and processes each gene.
"""
+ _select_config_for_runtime(argv)
+
+ import pandas as pd
+ from config.helpers import location
+ from config.config import parse_args, extract_config, log_config
+ from config.constants import (
+ NUM_TRAJECTORIES,
+ PARAMETER_SPACE,
+ PERTURBATIONS_VALUE,
+ Y_METRIC_DESCRIPTIONS,
+ validate_ode_inputs,
+ )
+ from config.logconf import setup_logger
+ from protwise.paramest.core import process_gene_wrapper
+ from protwise.plotting import Plotter
+ from common.utils import latexit
+ from common.results import populate_standard_subdirs
+ from common.utils.display import ensure_output_directory, save_result, organize_output_files, create_report, merge_obs_est
+
+ logger = setup_logger()
+ args = parse_args(argv)
+ config = extract_config(args)
+ if not config:
+ logger.error("Invalid configuration. Exiting.")
+ return
+
+ validate_ode_inputs(config)
+ out_dir, out_results_dir = initialize_run_contract(config, args, logger)
+
# Set up the logger
+ weights = config.get("weights", {})
+ sensitivity = config.get("sensitivity", {})
logger.info(" --------------------------------")
- logger.info(f"{model_type} Phosphorylation Modelling Configuration")
+ logger.info(f"{config['model_type']} Phosphorylation Modelling Configuration")
logger.info(" --------------------------------")
- log_config(logger, config['bounds'], args)
- logger.info(f" i = Number of phosphorylation sites (Residue_Position) in the model")
- logger.info(f" L2 Regularization: {USE_REGULARIZATION}")
- logger.info(f" Confidence Interval: {ALPHA_CI*100}")
+ log_config(logger, config["bounds"], args)
+ logger.info(" i = Number of phosphorylation sites (Residue_Position) in the model")
+ logger.info(f" L2 Regularization: {config['use_regularization']}")
+ logger.info(f" Confidence Interval: {config['alpha_ci'] * 100}")
logger.info(" --------------------------------")
logger.info(" Composite Scoring Function:")
logger.info(" score = α * RMSE + β * MAE + γ * Var(residuals) + δ * MSE + μ * L2 norm")
@@ -82,57 +144,49 @@ def main():
logger.info(" - L2 norm: L2 norm of parameter estimates")
logger.info(" --------------------------------")
logger.info(" Weights:")
- logger.info(f" - α (RMSE): {ALPHA_WEIGHT}")
- logger.info(f" - β (MAE): {BETA_WEIGHT}")
- logger.info(f" - γ (Var): {GAMMA_WEIGHT}")
- logger.info(f" - δ (MSE): {DELTA_WEIGHT}")
- logger.info(f" - μ (L2 norm): {MU_WEIGHT}")
+ logger.info(f" - α (RMSE): {weights.get('alpha')}")
+ logger.info(f" - β (MAE): {weights.get('beta')}")
+ logger.info(f" - γ (Var): {weights.get('gamma')}")
+ logger.info(f" - δ (MSE): {weights.get('delta')}")
+ logger.info(f" - μ (L2 norm): {weights.get('mu')}")
logger.info(" --------------------------------")
logger.info(" Lower score indicates a better fit.")
logger.info(" --------------------------------")
- logger.info(f" Sensitivity Analysis: {SENSITIVITY_ANALYSIS}")
- if SENSITIVITY_ANALYSIS:
- logger.info(f" - Metric: {' '.join(part.upper() for part in Y_METRIC.split('_'))}")
- logger.info(f" - {Y_METRIC_DESCRIPTIONS.get(Y_METRIC, 'No description available.')}")
- logger.info(f" - Number of Trajectories: {NUM_TRAJECTORIES}")
- logger.info(f" - Parameter Space: {PARAMETER_SPACE}")
- logger.info(f" - Perturbations: {PERTURBATIONS_VALUE}")
+ logger.info(f" Sensitivity Analysis: {sensitivity.get('enabled')}")
+ if sensitivity.get("enabled"):
+ metric = str(sensitivity.get("metric", "total_signal"))
+ logger.info(f" - Metric: {' '.join(part.upper() for part in metric.split('_'))}")
+ logger.info(f" - {Y_METRIC_DESCRIPTIONS.get(metric, 'No description available.')}")
+ logger.info(f" - Number of Trajectories: {sensitivity.get('num_trajectories', NUM_TRAJECTORIES)}")
+ logger.info(f" - Parameter Space: {sensitivity.get('num_levels', PARAMETER_SPACE)}")
+ logger.info(f" - Perturbations: {sensitivity.get('perturbation', PERTURBATIONS_VALUE)}")
logger.info(" --------------------------------")
- # Make output directory
- ensure_output_directory(OUT_DIR)
- # Load the data
- protein_data = pd.read_csv(config['input_excel_protein'])
- kinase_data = pd.read_excel(config['input_excel_psite'], sheet_name='Estimated')
- mrna_data = pd.read_excel(config['input_excel_rna'], sheet_name='Estimated')
+ ensure_output_directory(out_dir)
+
+ protein_data = pd.read_csv(config["input_excel_protein"])
+ kinase_data = pd.read_excel(config["input_excel_psite"], sheet_name="Estimated")
+ mrna_data = pd.read_excel(config["input_excel_rna"], sheet_name="Estimated")
- # Check if the data is empty
if mrna_data.empty and kinase_data.empty and protein_data.empty:
- logger.error("No data found in the input Excel files.")
+ logger.error("No data found in the input files.")
return
- # Check if the required columns are present: Gene, Psite, x1 - x14
- required_columns = ['Gene', 'Psite'] + [f'x{i}' for i in range(1, 15)]
+ required_columns = ["Gene", "Psite"] + [f"x{i}" for i in range(1, 15)]
missing_columns = [col for col in required_columns if col not in kinase_data.columns and col not in protein_data.columns]
if missing_columns:
logger.error(f"Missing columns in the phosphorylation data: {', '.join(missing_columns)}")
return
- # Check if the required columns are present in mRNA data: mRNA, x1 - x9
- required_mrna_columns = ['mRNA'] + [f'x{i}' for i in range(1, 10)]
+ required_mrna_columns = ["mRNA"] + [f"x{i}" for i in range(1, 10)]
missing_mrna_columns = [col for col in required_mrna_columns if col not in mrna_data.columns]
if missing_mrna_columns:
logger.error(f"Missing columns in the mRNA data: {', '.join(missing_mrna_columns)}")
return
- # Extract unique values from both datasets
- proteins = set(kinase_data['Gene'].dropna().unique())
- mrnas = set(mrna_data['mRNA'].dropna().unique())
-
- # Get sorted common proteins
+ proteins = set(kinase_data["Gene"].dropna().unique())
+ mrnas = set(mrna_data["mRNA"].dropna().unique())
common_proteins = sorted(proteins.intersection(mrnas))
-
- # Get sorted non-common proteins
non_common = sorted(proteins.symmetric_difference(mrnas))
if not common_proteins:
@@ -148,18 +202,15 @@ def main():
logger.info(" " + " ".join(f"[{gene}]" for gene in non_common))
logger.info(" --------------------------------")
- if DEV_TEST:
- # Load only gene 'X'
- _test = "ABL2"
- if _test in kinase_data["Gene"].values:
- genes = kinase_data[kinase_data["Gene"] == _test]["Gene"].unique().tolist()
+ if config.get("dev_test"):
+ test_gene = "ABL2"
+ if test_gene in kinase_data["Gene"].values:
+ genes = kinase_data[kinase_data["Gene"] == test_gene]["Gene"].unique().tolist()
else:
- raise ValueError(f"{_test} not found in the input data.")
+ raise ValueError(f"{test_gene} not found in the input data.")
else:
- # Load all protein groups
genes = common_proteins
- # Check if the genes are empty
if not genes:
logger.error("No genes found in the input data.")
return
@@ -168,56 +219,45 @@ def main():
for gene in genes:
logger.info(f"[{gene}] Processing...")
result = process_gene_wrapper(
- gene, protein_data, kinase_data, mrna_data, TIME_POINTS,
- config['bounds'], config['bootstraps']
+ gene,
+ protein_data,
+ kinase_data,
+ mrna_data,
+ config["time_points"],
+ config["bounds"],
+ config["bootstraps"],
+ out_dir=out_dir,
)
results.append(result)
- # Check if the results are empty
if not results:
logger.error("No results found after processing.")
return
- # Save the results
- save_result(results, excel_filename=OUT_RESULTS_DIR)
-
- # Merge the observed data and model fits for each gene
- merged_df = merge_obs_est(OUT_RESULTS_DIR)
-
- # Plot goodness of fit.
- Plotter("", OUT_DIR).plot_gof(merged_df)
-
- # Plot Kullback-Leibler divergence.
- Plotter("", OUT_DIR).plot_kld(merged_df)
-
- # Plot parameter relationships - profiles
- Plotter("", OUT_DIR).plot_top_param_pairs(OUT_RESULTS_DIR)
-
- # Plot regularization term values.
- Plotter("", OUT_DIR).plot_regularization(OUT_RESULTS_DIR)
-
- # Plot model protein wise errors.
- Plotter("", OUT_DIR).plot_model_error(OUT_RESULTS_DIR)
+ save_result(results, excel_filename=out_results_dir)
+ merged_df = merge_obs_est(out_results_dir)
+ Plotter("", out_dir).plot_gof(merged_df)
+ Plotter("", out_dir).plot_kld(merged_df)
+ Plotter("", out_dir).plot_top_param_pairs(out_results_dir)
+ Plotter("", out_dir).plot_regularization(out_results_dir)
+ Plotter("", out_dir).plot_model_error(out_results_dir)
logger.info("Plotting completed.")
- # LateX the results
- latexit.main(OUT_DIR)
-
- logger.info(f"LateX generated.")
+ latexit.main(out_dir)
+ logger.info("LateX generated.")
- # Organize output files and create a report
- organize_output_files([OUT_DIR])
- create_report(OUT_DIR)
+ organize_output_files([out_dir])
+ create_report(out_dir)
logger.info(" --------------------------------")
- logger.info(f" Report & Results {location(str(OUT_DIR))}")
-
- # Click to open the report in a web browser.
- for fpath in [OUT_DIR / 'report.html']:
+ logger.info(f" Report & Results {location(str(out_dir))}")
+ for fpath in [out_dir / "report.html"]:
logger.info(f" {fpath.as_uri()}")
-
logger.info(" --------------------------------")
+ populate_standard_subdirs(out_dir)
+
+
if __name__ == "__main__":
main()
diff --git a/scripts/analyze_tf_kin_counts.py b/scripts/analyze_tf_kin_counts.py
index df676dd..d57208c 100644
--- a/scripts/analyze_tf_kin_counts.py
+++ b/scripts/analyze_tf_kin_counts.py
@@ -11,6 +11,7 @@
Author: Abhinav Mishra
"""
+import argparse
from pathlib import Path
import pandas as pd
@@ -148,5 +149,20 @@ def main(
print(per_gene.head(10).to_string(index=False))
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ """Parse command-line arguments for dashboard and terminal execution."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--tfopt-xlsx", default="data/tfopt_results.xlsx", help="Path to TFOpt result workbook.")
+ parser.add_argument("--kinopt-xlsx", default="data/kinopt_results.xlsx", help="Path to KinOpt result workbook.")
+ parser.add_argument("--out-dir", default="results_scripts", help="Directory where analysis outputs are written.")
+ return parser.parse_args(argv)
+
+
+def cli(argv: list[str] | None = None) -> None:
+ """CLI entry point that forwards parsed arguments to the analysis function."""
+ args = parse_args(argv)
+ main(tfopt_xlsx=args.tfopt_xlsx, kinopt_xlsx=args.kinopt_xlsx, out_dir=args.out_dir)
+
+
if __name__ == "__main__":
- main()
+ cli()
diff --git a/scripts/compare_mechanisms.py b/scripts/compare_mechanisms.py
index ddffb61..85ef01d 100644
--- a/scripts/compare_mechanisms.py
+++ b/scripts/compare_mechanisms.py
@@ -1,3 +1,14 @@
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+if str(REPO_ROOT) not in sys.path:
+ sys.path.insert(0, str(REPO_ROOT))
+
+import json
+import inspect
import streamlit as st
import streamlit.components.v1 as components
@@ -11,20 +22,48 @@
import networkx as nx
from pathlib import Path
-import io
-import imageio.v2 as imageio
-
from networkmodel import config
+import logging
+import networkmodel.network as network_mod
+import networkmodel.simulate as simulate_mod
+import networkmodel.SteadyStateAnalysis as steady_mod
from networkmodel.network import Index, KinaseInput, System
-from networkmodel.simulate import simulate_and_measure
from networkmodel.SteadyStateAnalysis import simulate_until_steady
-from networkmodel.params import init_raw_params, unpack_params
from networkmodel.io import load_data
from networkmodel.BuildMatrix import build_W_parallel, build_tf_matrix
-from networkmodel.utils import normalize_fc_to_t0
+
+try:
+ import networkmodel.backend as backend_mod
+except Exception:
+ backend_mod = None
st.set_page_config(page_title="PhoskinTime Global Knockout", layout="wide")
+RESULTS_DIR_PICKED = Path("./results_network_combinatorial")
+MODEL_NAMES = {
+ 0: "distributive",
+ 1: "sequential",
+ 2: "combinatorial",
+ 4: "saturating",
+}
+
+INTENDED_MODEL = 2 # change to 2 for combinatorial
+
+def _force_networkmodel_model(model: int) -> None:
+ """Synchronize model selection across modules that imported MODEL at module scope."""
+ model = int(model)
+ config.MODEL = model
+ network_mod.MODEL = model
+ simulate_mod.MODEL = model
+
+ if hasattr(steady_mod, "MODEL"):
+ steady_mod.MODEL = model
+
+ if backend_mod is not None and hasattr(backend_mod, "MODEL"):
+ backend_mod.MODEL = model
+
+
+_force_networkmodel_model(INTENDED_MODEL)
def _standardize_tf_columns(df_tf: pd.DataFrame) -> pd.DataFrame:
"""
@@ -153,9 +192,72 @@ def _mechanistic_phospho_filter(df_kin: pd.DataFrame, df_pho: pd.DataFrame) -> p
keep = np.fromiter(((p, s) in kin_site_pairs for (p, s) in pairs), dtype=bool, count=len(pairs))
return df_pho2.loc[keep].copy()
+def _first_existing(*paths: Path) -> Path:
+ for p in paths:
+ if p.exists():
+ return p
+ raise FileNotFoundError("None of these files exist:\n" + "\n".join(str(p) for p in paths))
+
+
+def _load_fitted_params_picked(results_dir: Path, defaults: dict) -> dict:
+ path = _first_existing(
+ results_dir / "fitted_params_picked.json",
+ results_dir / "tables" / "fitted_params_picked.json",
+ )
+
+ with open(path, "r") as f:
+ raw = json.load(f)
+
+ # Handle either flat JSON or nested export JSON.
+ if "params" in raw and isinstance(raw["params"], dict):
+ raw = raw["params"]
+ elif "best_params" in raw and isinstance(raw["best_params"], dict):
+ raw = raw["best_params"]
+ elif "fitted_params" in raw and isinstance(raw["fitted_params"], dict):
+ raw = raw["fitted_params"]
+
+ params = {}
+
+ for key, default_value in defaults.items():
+ if key not in raw:
+ raise KeyError(f"{path} does not contain fitted parameter key: {key}")
+
+ value = raw[key]
+
+ if isinstance(default_value, np.ndarray):
+ arr = np.asarray(value, dtype=float)
+
+ if arr.shape != default_value.shape:
+ raise ValueError(
+ f"Shape mismatch for {key}: JSON has {arr.shape}, "
+ f"expected {default_value.shape}"
+ )
+
+ params[key] = arr
+ else:
+ params[key] = float(value)
+
+ return params
+
+def _load_picked_predictions(results_dir: Path):
+ wt_dfp = pd.read_csv(results_dir / "pred_prot_picked.csv")
+ wt_dfr = pd.read_csv(results_dir / "pred_rna_picked.csv")
+ wt_pho = pd.read_csv(results_dir / "pred_phospho_picked.csv")
+
+ for df in (wt_dfp, wt_dfr, wt_pho):
+ if "protein" in df.columns:
+ df["protein"] = df["protein"].astype(str).str.strip().str.upper()
+ if "psite" in df.columns:
+ df["psite"] = df["psite"].astype(str).str.strip()
+ if "time" in df.columns:
+ df["time"] = pd.to_numeric(df["time"], errors="coerce")
+ if "pred_fc" in df.columns:
+ df["pred_fc"] = pd.to_numeric(df["pred_fc"], errors="coerce")
+
+ return wt_dfp, wt_dfr, wt_pho
@st.cache_resource
-def load_system():
+def load_system(model: int):
"""
Caches and initializes a System object along with associated parameters, indices, and data models.
@@ -165,11 +267,22 @@ def load_system():
parameters derived from an optimization run, reconstructing necessary input for reanalysis or dashboarding.
Returns:
- tuple: A tuple containing the"""
- # IMPORTANT: ensure model selection matches run *before* building System
- config.MODEL = 0
+ tuple: A tuple containing the
+ """
+
+ # IMPORTANT: ensure model selection matches run before building Index/System.
+ _force_networkmodel_model(model)
+
+ model_name = MODEL_NAMES.get(model, f"unknown_MODEL_{model}")
+
+ logging.getLogger(__name__).warning(
+ "[Dashboard] Intended networkmodel MODEL=%d (%s); results_dir=%s",
+ model,
+ model_name,
+ RESULTS_DIR_PICKED,
+ )
- results_dir = Path("./results_model_global_distributive")
+ results_dir = RESULTS_DIR_PICKED
class Args:
kinase_net, tf_net = config.KINASE_NET_FILE, config.TF_NET_FILE
@@ -179,11 +292,6 @@ class Args:
df_kin, df_tf_raw, df_prot, df_pho, df_rna, kin_beta_map, tf_beta_map = load_data(Args())
- # optional (runner has a flag; keep identical default behavior = False)
- if getattr(Args, "normalize_fc_steady", False):
- df_prot = normalize_fc_to_t0(df_prot)
- df_pho = normalize_fc_to_t0(df_pho)
-
# runner: strict mechanistic phospho filter
df_pho = _mechanistic_phospho_filter(df_kin, df_pho)
@@ -236,21 +344,9 @@ class Args:
sys = System(idx, W_global, tf_mat, kin_in, defaults, tf_deg)
- # slices/schema must match the run that produced pareto_X.npy
- _, slices, _, _ = init_raw_params(defaults)
-
- X = np.load(results_dir / "pareto_X.npy")
- theta = X[0].astype(float)
-
- expected_dim = max(s.stop for s in slices.values())
- if theta.size != expected_dim:
- raise ValueError(
- f"Incompatible pareto_X dimension: got {theta.size}, expected {expected_dim}. "
- "Dashboard reconstruction does not match optimization run. "
- "Fix compare_mechanisms reconstruction or re-run optimization."
- )
-
- best_params = unpack_params(theta, slices)
+ # Load the exact parameter set used by the pipeline-selected solution.
+ # Do not use pareto_X[0]; it may not be the picked/best solution.
+ best_params = _load_fitted_params_picked(results_dir, defaults)
s_rates_path = results_dir / "S_rates_picked.csv"
s_rates = pd.read_csv(s_rates_path) if s_rates_path.exists() else pd.DataFrame()
@@ -260,96 +356,703 @@ class Args:
def run_sim(sys, idx, mod_params):
+ """Update system parameters and simulate with the intended dashboard model."""
+ _force_networkmodel_model(INTENDED_MODEL)
+
+ sys.update(**mod_params)
+ return simulate_mod.simulate_and_measure(
+ sys,
+ idx,
+ config.TIME_POINTS_PROTEIN,
+ config.TIME_POINTS_RNA,
+ config.TIME_POINTS_PHOSPHO,
+ )
+
+def extract_fc_from_Y(Y, idx, t, protein, normalize=True):
+ """Extract RNA, total protein, and phospho-site trajectories for one protein."""
+ p_idx = idx.p2i[protein]
+ st_y = idx.offset_y[p_idx]
+
+ rna_vals = Y[:, st_y]
+ prot_vals = Y[:, st_y + 1]
+
+ ns = idx.n_sites[p_idx]
+ site_names = idx.sites[p_idx]
+
+ if ns > 0:
+ psite_vals = Y[:, st_y + 2: st_y + 2 + ns]
+ phos_sum = np.sum(psite_vals, axis=1)
+ total_prot = prot_vals + phos_sum
+ else:
+ psite_vals = np.zeros((len(t), 0))
+ total_prot = prot_vals
+
+ if normalize:
+ rna_base = rna_vals[0] if abs(rna_vals[0]) > 1e-12 else 1.0
+ prot_base = total_prot[0] if abs(total_prot[0]) > 1e-12 else 1.0
+
+ rna_vals = rna_vals / rna_base
+ total_prot = total_prot / prot_base
+
+ if ns > 0:
+ site_base = psite_vals[0, :].copy()
+ site_base[np.abs(site_base) < 1e-12] = 1.0
+ psite_vals = psite_vals / site_base
+
+ df = pd.DataFrame(
+ {
+ "time": t,
+ "rna": rna_vals,
+ "protein": total_prot,
+ }
+ )
+
+ if ns > 0:
+ df_ps = pd.DataFrame(psite_vals, columns=site_names)
+ df_ps["time"] = t
+ df_long = df_ps.melt(
+ id_vars="time",
+ var_name="psite",
+ value_name="psite_value",
+ )
+ df = df.merge(df_long, on="time", how="left")
+
+ return df
+def _safe_min_max(values: np.ndarray) -> tuple[float, float]:
+ """Return finite min/max values for dashboard diagnostics."""
+ arr = np.asarray(values, dtype=float)
+ finite = arr[np.isfinite(arr)]
+ if finite.size == 0:
+ return float("nan"), float("nan")
+ return float(np.min(finite)), float(np.max(finite))
+
+
+def _simulate_until_steady_optional_y0(sys_obj, t_max: float, n_points: int, y0: np.ndarray | None = None):
+ """
+ Call simulate_until_steady with a fixed initial state if the installed function exposes
+ such an argument. Otherwise fall back to the normal call.
+
+ This keeps compare_mechanisms.py script-only and avoids modifying networkmodel modules.
"""
- Updates a system with given modification parameters and executes a simulation to return measurements.
+ sig = inspect.signature(simulate_until_steady)
+ kwargs = {
+ "t_max": float(t_max),
+ "n_points": int(n_points),
+ }
- This function takes a system object, updates its attributes based on the provided
- modification parameters, and runs a simulation. Measurements for protein, RNA, and
- phosphorylation levels are taken at predefined time points.
+ used_fixed_y0 = False
- Args:
- sys: The system object to be updated and simulated.
- idx: An integer index specifying which part of the system to simulate.
- mod_params: A dictionary containing the modification parameters to update
- the system with.
+ if y0 is not None:
+ for y0_name in ("y0", "initial_state", "initial_conditions", "y_init"):
+ if y0_name in sig.parameters:
+ kwargs[y0_name] = np.asarray(y0, dtype=float)
+ used_fixed_y0 = True
+ break
- Returns:
- The measurements obtained from the simulation"""
- sys.update(**mod_params)
- return simulate_and_measure(sys, idx, config.TIME_POINTS_PROTEIN, config.TIME_POINTS_RNA,
- config.TIME_POINTS_PHOSPHO)
+ t, Y = simulate_until_steady(sys_obj, **kwargs)
+ return t, Y, used_fixed_y0
+
+
+def _align_Y_to_reference_initial(
+ Y: np.ndarray,
+ Y_ref: np.ndarray,
+ eps: float = 1e-12,
+) -> np.ndarray:
+ """
+ Align a trajectory to the reference initial state.
+
+ This removes artificial t=0 inflation caused by separately initialized
+ perturbation trajectories. Multiplicative alignment preserves non-negativity
+ and relative dynamic shape better than a blind additive shift.
+ """
+ Y = np.asarray(Y, dtype=float)
+ Y_ref = np.asarray(Y_ref, dtype=float)
+
+ if Y.ndim != 2 or Y_ref.ndim != 2:
+ raise ValueError(f"Expected 2D trajectories, got Y={Y.shape}, Y_ref={Y_ref.shape}")
+
+ if Y.shape[1] != Y_ref.shape[1]:
+ raise ValueError(
+ f"State dimension mismatch: KO has {Y.shape[1]} states, WT has {Y_ref.shape[1]} states"
+ )
+
+ Y_aligned = Y.copy()
+
+ ko0 = Y[0, :].astype(float)
+ wt0 = Y_ref[0, :].astype(float)
+
+ scale = np.ones_like(ko0, dtype=float)
+ can_scale = np.abs(ko0) > eps
+
+ scale[can_scale] = wt0[can_scale] / ko0[can_scale]
+ Y_aligned[:, can_scale] = Y[:, can_scale] * scale[can_scale]
+
+ # If a KO initial state is numerically zero but WT is not, use additive correction.
+ cannot_scale = ~can_scale
+ if np.any(cannot_scale):
+ Y_aligned[:, cannot_scale] = Y[:, cannot_scale] + (wt0[cannot_scale] - ko0[cannot_scale])
+
+ # Enforce exact equality at t=0 after numerical alignment.
+ Y_aligned[0, :] = wt0
+
+ return Y_aligned
+
+
+def _run_forward_wt_ko_for_display(
+ sys_obj,
+ best_params: dict,
+ ko_params: dict,
+ t_max: float,
+ n_points: int,
+):
+ """
+ Run WT and KO forward simulations for the dashboard.
+
+ WT is the fitted baseline.
+ KO is simulated with perturbed parameters, then displayed from the WT fitted
+ initial state. If simulate_until_steady supports y0, that is used directly.
+ Otherwise the KO trajectory is state-aligned for display.
+ """
+ sys_obj.update(**best_params)
+ t_wt, Y_wt, _ = _simulate_until_steady_optional_y0(
+ sys_obj,
+ t_max=t_max,
+ n_points=n_points,
+ y0=None,
+ )
+
+ Y_wt = np.asarray(Y_wt, dtype=float)
+ wt_y0 = Y_wt[0, :].copy()
+
+ sys_obj.update(**ko_params)
+ t_ko, Y_ko_raw, used_fixed_y0 = _simulate_until_steady_optional_y0(
+ sys_obj,
+ t_max=t_max,
+ n_points=n_points,
+ y0=wt_y0,
+ )
+
+ Y_ko_raw = np.asarray(Y_ko_raw, dtype=float)
+
+ if used_fixed_y0:
+ Y_ko_plot = Y_ko_raw.copy()
+ Y_ko_plot[0, :] = wt_y0
+ alignment_mode = "fixed y0 passed to simulate_until_steady"
+ else:
+ Y_ko_plot = _align_Y_to_reference_initial(Y_ko_raw, Y_wt)
+ alignment_mode = "post-simulation WT-initial-state alignment"
+
+ sys_obj.update(**best_params)
+
+ return {
+ "t_fine": np.asarray(t_wt, dtype=float),
+ "Y_wt": Y_wt,
+ "t_ko": np.asarray(t_ko, dtype=float),
+ "Y_ko_raw": Y_ko_raw,
+ "Y_ko_plot": Y_ko_plot,
+ "used_fixed_y0": used_fixed_y0,
+ "alignment_mode": alignment_mode,
+ }
+
+
+def extract_phosphosite_states_from_Y(
+ Y: np.ndarray,
+ idx: Index,
+ t: np.ndarray,
+ protein: str,
+ normalize_to_t0: bool = False,
+ reference_raw_states: np.ndarray | None = None,
+) -> tuple[pd.DataFrame, np.ndarray, np.ndarray]:
+ """
+ Extract raw or reference-normalized phosphosite ODE states for one protein.
+
+ This intentionally reads directly from the ODE state vector. It does not use
+ exported fitted predictions, pred_fc, or simulate_and_measure() output.
+
+ If reference_raw_states is provided, normalization uses its t0 denominator.
+ That is required for intervention plots: WT and KO must use the same WT
+ t0 phosphosite reference.
+ """
+ p_idx = idx.p2i[protein]
+ ns = int(idx.n_sites[p_idx])
+ site_names = list(idx.sites[p_idx])
+
+ if ns <= 0:
+ empty = pd.DataFrame(columns=["time", "psite", "psite_value"])
+ return empty, np.zeros((len(t), 0)), np.zeros((len(t), 0))
+
+ start = int(idx.offset_y[p_idx]) + 2
+ stop = start + ns
+
+ raw_states = np.asarray(Y[:, start:stop], dtype=float)
+ plotted_states = raw_states.copy()
+
+ if normalize_to_t0:
+ ref = raw_states if reference_raw_states is None else np.asarray(reference_raw_states, dtype=float)
+
+ if ref.shape[1] != raw_states.shape[1]:
+ raise ValueError(
+ f"Reference phosphosite state dimension mismatch: ref={ref.shape}, raw={raw_states.shape}"
+ )
+
+ denom = ref[0, :].copy()
+ denom[np.abs(denom) < 1e-12] = 1.0
+ plotted_states = raw_states / denom
+
+ df = pd.DataFrame(plotted_states, columns=site_names)
+ df["time"] = np.asarray(t, dtype=float)
+
+ df_long = df.melt(
+ id_vars="time",
+ var_name="psite",
+ value_name="psite_value",
+ )
+
+ return df_long, raw_states, plotted_states
+
+def _extract_raw_phosphosite_matrix(
+ Y: np.ndarray,
+ idx: Index,
+ protein: str,
+) -> tuple[list[str], np.ndarray]:
+ """Return raw phosphosite ODE states for one protein as (site_names, matrix)."""
+ p_idx = idx.p2i[protein]
+ ns = int(idx.n_sites[p_idx])
+ site_names = list(idx.sites[p_idx])
+
+ if ns <= 0:
+ return [], np.zeros((Y.shape[0], 0), dtype=float)
+
+ start = int(idx.offset_y[p_idx]) + 2
+ stop = start + ns
+
+ return site_names, np.asarray(Y[:, start:stop], dtype=float)
+
+
+def _interp_state_matrix(
+ t_src: np.ndarray,
+ values_src: np.ndarray,
+ t_target: np.ndarray,
+) -> np.ndarray:
+ """Interpolate each state column onto requested target times."""
+ t_src = np.asarray(t_src, dtype=float)
+ t_target = np.asarray(t_target, dtype=float)
+ values_src = np.asarray(values_src, dtype=float)
+
+ if values_src.size == 0:
+ return np.zeros((len(t_target), 0), dtype=float)
+
+ order = np.argsort(t_src)
+ t_src = t_src[order]
+ values_src = values_src[order, :]
+
+ out = np.empty((len(t_target), values_src.shape[1]), dtype=float)
+
+ for j in range(values_src.shape[1]):
+ out[:, j] = np.interp(t_target, t_src, values_src[:, j])
+
+ return out
+
+
+def _build_inspector_ko_phosphosite_intervention_df(
+ sys_obj,
+ idx: Index,
+ best_params: dict,
+ ko_params: dict,
+ protein: str,
+ wt_pho_data: pd.DataFrame,
+ n_points: int = 1000,
+) -> tuple[pd.DataFrame, pd.DataFrame]:
+ """
+ Build KO phosphosite inspector values on the same scale as WT picked pred_fc.
+
+ Correct scale rule:
+ KO_display_fc(t) = WT_picked_pred_fc(t) * KO_raw_state(t) / WT_raw_state(t)
+
+ This avoids the bad t0 normalization:
+ KO_raw_state(t) / WT_raw_state(t0)
+
+ which can inflate values to 20-50 when WT t0 state is around 0.01.
+ """
+ if wt_pho_data is None or wt_pho_data.empty:
+ empty = pd.DataFrame(columns=["time", "psite", "psite_value"])
+ debug = pd.DataFrame()
+ return empty, debug
+
+ required = {"protein", "psite", "time", "pred_fc"}
+ missing = required - set(wt_pho_data.columns)
+ if missing:
+ raise ValueError(
+ f"wt_pho_data is missing columns required for phosphosite inspector: {missing}"
+ )
+
+ target_times = np.asarray(
+ sorted(wt_pho_data["time"].dropna().unique()),
+ dtype=float,
+ )
+
+ if target_times.size == 0:
+ empty = pd.DataFrame(columns=["time", "psite", "psite_value"])
+ debug = pd.DataFrame()
+ return empty, debug
+ t_max_local = float(np.max(target_times))
+ n_points_local = max(int(n_points), int(target_times.size) * 50)
+
+ forward = _run_forward_wt_ko_for_display(
+ sys_obj=sys_obj,
+ best_params=best_params,
+ ko_params=ko_params,
+ t_max=t_max_local,
+ n_points=n_points_local,
+ )
+
+ t_wt = np.asarray(forward["t_fine"], dtype=float)
+ Y_wt = np.asarray(forward["Y_wt"], dtype=float)
+
+ t_ko = np.asarray(forward["t_ko"], dtype=float)
+ Y_ko_plot = np.asarray(forward["Y_ko_plot"], dtype=float)
+
+ site_names, raw_wt_full = _extract_raw_phosphosite_matrix(Y_wt, idx, protein)
+ _, raw_ko_full = _extract_raw_phosphosite_matrix(Y_ko_plot, idx, protein)
+
+ if not site_names:
+ empty = pd.DataFrame(columns=["time", "psite", "psite_value"])
+ debug = pd.DataFrame()
+ return empty, debug
+
+ raw_wt_at_obs = _interp_state_matrix(t_wt, raw_wt_full, target_times)
+ raw_ko_at_obs = _interp_state_matrix(t_ko, raw_ko_full, target_times)
+
+ rows = []
+ eps = 1e-12
+
+ for j, site in enumerate(site_names):
+ wt_site = wt_pho_data[wt_pho_data["psite"].astype(str) == str(site)].copy()
+
+ if wt_site.empty:
+ continue
+
+ wt_site["time"] = pd.to_numeric(wt_site["time"], errors="coerce")
+ wt_site["pred_fc"] = pd.to_numeric(wt_site["pred_fc"], errors="coerce")
+ wt_site = wt_site.dropna(subset=["time", "pred_fc"])
+
+ if wt_site.empty:
+ continue
+
+ # Map observation time -> row in target_times.
+ time_to_i = {float(t): i for i, t in enumerate(target_times)}
+
+ for r in wt_site.itertuples(index=False):
+ t_val = float(r.time)
+ i = time_to_i[t_val]
+
+ wt_raw = float(raw_wt_at_obs[i, j])
+ ko_raw = float(raw_ko_at_obs[i, j])
+ wt_picked_fc = float(r.pred_fc)
+
+ if abs(wt_raw) < eps:
+ # Do not divide by a near-zero state. If WT raw is too small,
+ # treat KO effect ratio as neutral rather than creating inflation.
+ effect_ratio = 1.0
+ else:
+ effect_ratio = ko_raw / wt_raw
+
+ ko_display_fc = wt_picked_fc * effect_ratio
+
+ rows.append(
+ {
+ "protein": protein,
+ "psite": str(site),
+ "time": t_val,
+ "psite_value": ko_display_fc,
+ "wt_picked_fc": wt_picked_fc,
+ "wt_raw_state": wt_raw,
+ "ko_raw_state": ko_raw,
+ "effect_ratio_ko_over_wt": effect_ratio,
+ }
+ )
+
+ ko_long = pd.DataFrame(rows)
+
+ if raw_wt_full.shape == raw_ko_full.shape and raw_wt_full.size:
+ t0_max_abs_diff = float(np.max(np.abs(raw_wt_full[0, :] - raw_ko_full[0, :])))
+ else:
+ t0_max_abs_diff = float("nan")
+
+ if ko_long.empty:
+ debug = pd.DataFrame()
+ else:
+ debug = pd.DataFrame(
+ {
+ "metric": [
+ "WT raw phosphosite min",
+ "WT raw phosphosite max",
+ "KO displayed raw phosphosite min",
+ "KO displayed raw phosphosite max",
+ "KO/WT raw effect ratio min",
+ "KO/WT raw effect ratio max",
+ "KO inspector displayed FC min",
+ "KO inspector displayed FC max",
+ "max abs WT-vs-KO phosphosite difference at t0",
+ ],
+ "value": [
+ float(np.nanmin(raw_wt_full)),
+ float(np.nanmax(raw_wt_full)),
+ float(np.nanmin(raw_ko_full)),
+ float(np.nanmax(raw_ko_full)),
+ float(np.nanmin(ko_long["effect_ratio_ko_over_wt"])),
+ float(np.nanmax(ko_long["effect_ratio_ko_over_wt"])),
+ float(np.nanmin(ko_long["psite_value"])),
+ float(np.nanmax(ko_long["psite_value"])),
+ t0_max_abs_diff,
+ ],
+ }
+ )
+
+ return ko_long, debug
# --- UI Setup ---
st.title("🧪 Global Signaling & Transcriptional Knockout Explorer")
-sys, idx, best_params, df_tf_model, s_rates = load_system()
+st.caption("This dashboard builds the networkmodel system and runs WT/KO simulations on demand.")
+
+with st.sidebar:
+ if st.button("Clear cached model", key="clear-compare-cache"):
+ st.cache_resource.clear()
+ st.session_state.pop("compare_mechanisms_started", None)
+ st.rerun()
+
+ start_dashboard = st.button(
+ "Load model and start dashboard",
+ type="primary",
+ key="start-compare-mechanisms",
+ )
+
+if start_dashboard:
+ st.session_state["compare_mechanisms_started"] = True
+
+if not st.session_state.get("compare_mechanisms_started", False):
+ st.info("Click **Load model and start dashboard** in the sidebar. First load may take several minutes.")
+ st.stop()
+
+with st.spinner("Loading data, building network matrices, and reconstructing fitted parameters..."):
+ sys, idx, best_params, df_tf_model, s_rates = load_system(INTENDED_MODEL)
+
+model_name = MODEL_NAMES.get(INTENDED_MODEL, f"unknown_MODEL_{INTENDED_MODEL}")
+
+if not st.session_state.get("model_banner_shown", False):
+ st.success(
+ f"Loaded intended networkmodel: MODEL={INTENDED_MODEL} ({model_name}); "
+ f"results_dir={RESULTS_DIR_PICKED}"
+ )
+ st.session_state["model_banner_shown"] = True
+
+st.sidebar.info(f"Model: MODEL={INTENDED_MODEL} ({model_name})")
st.sidebar.header("🕹️ Control Panel")
-ko_type = st.sidebar.selectbox("1. Choose Perturbation Type", ["None", "Protein (Synthesis)", "Kinase (Activity)"])
-ko_params = {k: v.copy() if isinstance(v, np.ndarray) else v for k, v in best_params.items()}
+ko_params = {
+ k: v.copy() if isinstance(v, np.ndarray) else float(v) if np.isscalar(v) else v
+ for k, v in best_params.items()
+}
-Kmat_backup = None # default
+st.sidebar.subheader("Perturbations")
+def _scale_protein_param(param_name: str, proteins: list[str], factor: float) -> None:
+ if param_name not in ko_params or not proteins:
+ return
+ ids = [idx.p2i[p] for p in proteins if p in idx.p2i]
+ if ids:
+ ko_params[param_name][ids] *= float(factor)
-def restore_Kinase():
- """
- Restores the state or data related to Kinase to its default or initial configuration.
- This function is designed to reset or reconstruct necessary elements connected
- to Kinase, which can include internal systems, data, or processes, based on
- the implementation.
+def _scale_kinase_param(param_name: str, kinases: list[str], factor: float) -> None:
+ if param_name not in ko_params or not kinases:
+ return
+ ids = [idx.k2i[k] for k in kinases if k in idx.k2i]
+ if ids:
+ ko_params[param_name][ids] *= float(factor)
- Returns:
- None: This function does not return any value.
- """
- return None
+def _site_rows_for_proteins(proteins: list[str]) -> list[int]:
+ rows: list[int] = []
+ for p in proteins:
+ if p not in idx.p2i:
+ continue
-if ko_type == "Protein (Synthesis)":
- target = st.sidebar.selectbox("Select Target Protein", idx.proteins)
- p_idx = idx.p2i[target]
- scale = st.sidebar.slider("Protein Synthesis Scale (0 = KO, 1 = WT)", 0.0, 1.0, 0.0, 0.05)
- ko_params["A_i"][p_idx] *= scale # scale synthesis rate
- sys.update(**ko_params)
+ p_i = idx.p2i[p]
+ start = int(idx.offset_s[p_i])
+ stop = start + int(idx.n_sites[p_i])
+ rows.extend(range(start, stop))
-elif ko_type == "Kinase (Activity)":
- target = st.sidebar.selectbox("Select Kinase to Inhibit", idx.kinases)
- k_idx = idx.k2i[target]
- scale = st.sidebar.slider("Kinase Activity Scale (0 = KO, 1 = WT)", 0.0, 1.0, 0.0, 0.05)
+ return rows
- # Backup Kmat and c_k for later restoration
- Kmat_backup = sys.kin.Kmat.copy()
- c_k_backup = sys.c_k.copy()
- # Apply multiplicative inhibition to both dynamic and static components
- sys.kin.Kmat[k_idx, :] *= scale
- ko_params["c_k"][k_idx] *= scale
+def _scale_site_param_by_protein(param_name: str, proteins: list[str], factor: float) -> None:
+ if param_name not in ko_params or not proteins:
+ return
+ rows = _site_rows_for_proteins(proteins)
+ if rows:
+ ko_params[param_name][rows] *= float(factor)
- # Restore both after KO simulation
- def restore_Kinase():
- """
- Restores the kinase matrix and kinase concentration values to their original backup states.
+with st.sidebar.expander("Kinase activity", expanded=True):
+ kinase_activity_targets = st.multiselect(
+ "Inhibit kinase activity",
+ options=list(idx.kinases),
+ key="ko_kinase_activity_targets",
+ )
+ kinase_activity_scale = st.slider(
+ "Kinase activity scale",
+ 0.0, 1.0, 0.0, 0.05,
+ key="ko_kinase_activity_scale",
+ help="0 = full kinase activity inhibition, 1 = WT",
+ )
- This function resets specific global system variables to their previously saved states using backup
- values. It is particularly useful for restoring system consistency after temporary modifications.
+with st.sidebar.expander("mRNA kinetics", expanded=False):
+ mrna_synthesis_targets = st.multiselect(
+ "Inhibit mRNA synthesis",
+ options=list(idx.proteins),
+ key="ko_mrna_synthesis_targets",
+ )
+ mrna_synthesis_scale = st.slider(
+ "mRNA synthesis scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_mrna_synthesis_scale",
+ )
- Raises:
- AttributeError: If any of the required backup attributes are not available or have been removed
- from the system.
- """
- sys.kin.Kmat = Kmat_backup
- sys.c_k = c_k_backup
+ mrna_degradation_targets = st.multiselect(
+ "Inhibit mRNA degradation",
+ options=list(idx.proteins),
+ key="ko_mrna_degradation_targets",
+ )
+ mrna_degradation_scale = st.slider(
+ "mRNA degradation scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_mrna_degradation_scale",
+ )
+with st.sidebar.expander("Protein kinetics", expanded=True):
+ protein_synthesis_targets = st.multiselect(
+ "Inhibit protein synthesis / translation",
+ options=list(idx.proteins),
+ key="ko_protein_synthesis_targets",
+ )
+ protein_synthesis_scale = st.slider(
+ "Protein synthesis scale",
+ 0.0, 1.0, 0.0, 0.05,
+ key="ko_protein_synthesis_scale",
+ help="0 = full translation/protein synthesis inhibition, 1 = WT",
+ )
- sys.update(**ko_params)
-else:
- target = None
+ protein_degradation_targets = st.multiselect(
+ "Inhibit protein degradation",
+ options=list(idx.proteins),
+ key="ko_protein_degradation_targets",
+ )
+ protein_degradation_scale = st.slider(
+ "Protein degradation scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_protein_degradation_scale",
+ )
+
+with st.sidebar.expander("Phospho-site turnover", expanded=False):
+ phospho_loss_targets = st.multiselect(
+ "Inhibit dephosphorylation / phosphodegradation for proteins",
+ options=list(idx.proteins),
+ key="ko_phospho_loss_targets",
+ )
+ phospho_loss_scale = st.slider(
+ "Phospho-loss scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_phospho_loss_scale",
+ help="0 = block phospho-site loss, 1 = WT",
+ )
+
+with st.sidebar.expander("Transcriptional regulation", expanded=False):
+ tf_efficacy_targets = st.multiselect(
+ "Inhibit TF/transcriptional efficacy",
+ options=list(idx.proteins),
+ key="ko_tf_efficacy_targets",
+ )
+ tf_efficacy_scale = st.slider(
+ "TF efficacy scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_tf_efficacy_scale",
+ )
+
+ tf_global_scale = st.slider(
+ "Global TF-drive scale",
+ 0.0, 1.0, 1.0, 0.05,
+ key="ko_tf_global_scale",
+ )
+
+
+# Apply perturbations.
+# Do not mutate sys.kin.Kmat or W_global.
+# Keep topology fixed; perturb only fitted dynamic parameters.
+
+_scale_kinase_param("c_k", kinase_activity_targets, kinase_activity_scale)
-# --- Simulation Logic ---
-wt_dfp, wt_dfr, wt_pho = run_sim(sys, idx, best_params)
-ko_dfp, ko_dfr, ko_pho = run_sim(sys, idx, ko_params)
+_scale_protein_param("A_i", mrna_synthesis_targets, mrna_synthesis_scale)
+_scale_protein_param("B_i", mrna_degradation_targets, mrna_degradation_scale)
+
+_scale_protein_param("C_i", protein_synthesis_targets, protein_synthesis_scale)
+_scale_protein_param("D_i", protein_degradation_targets, protein_degradation_scale)
+
+_scale_site_param_by_protein("Dp_i", phospho_loss_targets, phospho_loss_scale)
+
+_scale_protein_param("E_i", tf_efficacy_targets, tf_efficacy_scale)
+
+if "tf_scale" in ko_params:
+ ko_params["tf_scale"] = float(ko_params["tf_scale"]) * float(tf_global_scale)
+
+
+all_selected_targets = (
+ list(kinase_activity_targets)
+ + list(mrna_synthesis_targets)
+ + list(mrna_degradation_targets)
+ + list(protein_synthesis_targets)
+ + list(protein_degradation_targets)
+ + list(phospho_loss_targets)
+ + list(tf_efficacy_targets)
+)
+
+has_global_tf_perturbation = abs(float(tf_global_scale) - 1.0) > 1e-12
+has_perturbation = bool(all_selected_targets) or has_global_tf_perturbation
+
+ko_type = "Multi-perturbation" if has_perturbation else "None"
+target = all_selected_targets[0] if all_selected_targets else None
+scale = 1.0
+
+perturbation_signature = {
+ "kinase_activity": (tuple(kinase_activity_targets), float(kinase_activity_scale)),
+ "mrna_synthesis": (tuple(mrna_synthesis_targets), float(mrna_synthesis_scale)),
+ "mrna_degradation": (tuple(mrna_degradation_targets), float(mrna_degradation_scale)),
+ "protein_synthesis": (tuple(protein_synthesis_targets), float(protein_synthesis_scale)),
+ "protein_degradation": (tuple(protein_degradation_targets), float(protein_degradation_scale)),
+ "phospho_loss": (tuple(phospho_loss_targets), float(phospho_loss_scale)),
+ "tf_efficacy": (tuple(tf_efficacy_targets), float(tf_efficacy_scale)),
+ "tf_global": float(tf_global_scale),
+}
+
+with st.spinner("Loading picked WT predictions and running perturbation..."):
+ wt_dfp, wt_dfr, wt_pho = _load_picked_predictions(RESULTS_DIR_PICKED)
+
+ if ko_type == "None":
+ ko_dfp = wt_dfp.copy()
+ ko_dfr = wt_dfr.copy()
+ ko_pho = wt_pho.copy()
+ else:
+ ko_dfp, ko_dfr, _ko_pho_bad_fc = run_sim(sys, idx, ko_params)
+
+ # Do not trust phospho pred_fc from simulate_and_measure here.
+ # It is using a different/too-small FC denominator in this dashboard path.
+ ko_pho = _ko_pho_bad_fc.copy()
+
+
+# Leave the mutable System in a known baseline state after the comparison.
+sys.update(**best_params)
# --- Versatile Visualization: The Impact Scatter ---
st.header("🎯 System-Wide Impact Analysis")
@@ -567,210 +1270,16 @@ def restore_Kinase():
st.divider()
+st.header("Data - Fit Inspector")
selected_p = st.selectbox("Select a protein to inspect in detail:", idx.proteins)
+wt_p_data = wt_dfp[wt_dfp["protein"] == selected_p]
+ko_p_data = ko_dfp[ko_dfp["protein"] == selected_p]
+wt_r_data = wt_dfr[wt_dfr["protein"] == selected_p]
+ko_r_data = ko_dfr[ko_dfr["protein"] == selected_p]
+wt_pho_data = wt_pho[wt_pho["protein"] == selected_p]
+ko_pho_data = ko_pho[ko_pho["protein"] == selected_p]
-# --- Forward Simulation Panel with Finer Resolution ---
-st.subheader(f"🔁 Forward Simulation of {selected_p}")
-
-# Slider for t_max (in minutes): range from 1 hour to 14 days
-t_max = st.slider(
- "Simulation Time (minutes)",
- min_value=2,
- max_value=14 * 24 * 60, # 14 days
- value=960, # default: 16 hrs
- step=60
-)
-
-n_points = 10000
-
-# Simulate WT to steady state
-t_fine, Y_wt = simulate_until_steady(sys, t_max=t_max, n_points=n_points)
-
-
-# Extract per-protein output
-def extract_fc_from_Y(Y, idx, t, protein, normalize=True):
- """
- Extracts and processes feature components from a dataset for a given protein.
-
- This function retrieves RNA, protein, and phosphorylation site data associated with
- a specified protein from a dataset. It supports normalization of input data and
- returns a processed DataFrame containing time-series data for RNA, total protein,
- and (if available) phosphorylation site values.
-
- Args:
- Y: ndarray
- Input dataset containing RNA, protein, and phosphorylation site measurements.
- The dimensions of `Y` include time points as rows and feature components as
- columns.
- idx: object
- An"""
- p_idx = idx.p2i[protein]
- st_y = idx.offset_y[p_idx]
- rna_vals = Y[:, st_y]
- prot_vals = Y[:, st_y + 1]
- ns = idx.n_sites[p_idx]
- site_names = idx.sites[p_idx]
-
- if ns > 0:
- psite_vals = Y[:, st_y + 2: st_y + 2 + ns]
- phos_sum = np.sum(psite_vals, axis=1)
- total_prot = prot_vals + phos_sum
- else:
- psite_vals = np.zeros((len(t), 0))
- total_prot = prot_vals
-
- if normalize:
- rna_vals = rna_vals / rna_vals[0]
- total_prot = total_prot / total_prot[0]
- if ns > 0:
- psite_vals = psite_vals / psite_vals[0, :] # normalize each site
-
- # Base result
- df = pd.DataFrame({
- "time": t,
- "rna": rna_vals,
- "protein": total_prot,
- })
-
- # Add phospho sites in long format
- if ns > 0:
- df_ps = pd.DataFrame(psite_vals, columns=site_names)
- df_ps["time"] = t
- df_long = df_ps.melt(id_vars="time", var_name="psite", value_name="psite_value")
- df = df.merge(df_long, on="time", how="left")
-
- return df
-
-
-# WT values
-df_wt = extract_fc_from_Y(Y_wt, idx, t_fine, selected_p)
-
-# KO simulation
-sys.update(**ko_params)
-t_ko, Y_ko = simulate_until_steady(sys, t_max=t_max, n_points=n_points)
-df_ko = extract_fc_from_Y(Y_ko, idx, t_ko, selected_p)
-sys.update(**best_params) # restore baseline
-
-# --- Plot mRNA
-col1, col2 = st.columns(2)
-
-with col1:
- fig_fine_r = go.Figure()
- fig_fine_r.add_trace(go.Scatter(x=df_wt["time"], y=df_wt["rna"], name="WT", line=dict(color="black", dash="dash")))
- fig_fine_r.add_trace(go.Scatter(x=df_ko["time"], y=df_ko["rna"], name="KO", line=dict(color="red")))
- fig_fine_r.update_layout(title="mRNA Simulation", xaxis_title="Time", yaxis_title="Fold Change",
- template="plotly_white")
- fig_fine_r.update_xaxes(type="log")
- st.plotly_chart(fig_fine_r, use_container_width=True)
-
-# --- Plot Protein
-with col2:
- fig_fine_p = go.Figure()
- fig_fine_p.add_trace(
- go.Scatter(x=df_wt["time"], y=df_wt["protein"], name="WT", line=dict(color="black", dash="dash")))
- fig_fine_p.add_trace(go.Scatter(x=df_ko["time"], y=df_ko["protein"], name="KO", line=dict(color="blue")))
- fig_fine_p.update_layout(title="Protein Simulation", xaxis_title="Time", yaxis_title="Fold Change",
- template="plotly_white")
- fig_fine_p.update_xaxes(type="log")
- st.plotly_chart(fig_fine_p, use_container_width=True)
-
-col1, col2 = st.columns(2)
-
-# --- Signaling Drive Panel (Phosphorylation S)
-with col1:
- if selected_p in idx.p2i:
- p_idx = idx.p2i[selected_p]
- ns = idx.n_sites[p_idx]
-
- # Simulate WT again (ensures S is from WT context)
- t_S, Y_S = t_ko, Y_ko # Use already simulated KO values
- kin_vals = Y_S[:, [idx.k2i[k] for k in idx.kinases]].T # (n_kinases, time)
- kin_scaled = kin_vals * sys.c_k[:, None]
- S_t = sys.W_global @ kin_scaled # (n_sites, time)
-
- site_names, site_rows = [], []
-
- # Recompute correct global site indices for selected_p
- site_counter = 0
- for i, p in enumerate(idx.proteins):
- for j, site in enumerate(idx.sites[i]):
- if p == selected_p:
- site_names.append(site)
- site_rows.append(site_counter)
- site_counter += 1
-
- fig_s_time = go.Figure()
-
- for site_name, site_idx in zip(site_names, site_rows):
- color = px.colors.qualitative.Plotly[hash(site_name) % len(px.colors.qualitative.Plotly)]
- fig_s_time.add_trace(go.Scatter(
- x=t_S,
- y=S_t[site_idx, :],
- name=site_name,
- mode="lines",
- line=dict(dash="solid", color=color),
- opacity=0.9,
- ))
-
- fig_s_time.update_layout(
- title=f"{selected_p} – Phosphorylation (S)",
- xaxis_title="Time (min)",
- yaxis_title="S (Signaling Rate)",
- template="plotly_white"
- )
- fig_s_time.update_xaxes(type="log")
- st.plotly_chart(fig_s_time, use_container_width=True)
- else:
- st.warning(f"{selected_p} not found in protein index.")
-
-# Plot Phospho-sites states from simulation
-with col2:
- fig_sites_fine = go.Figure()
-
- # Safety check for valid data
- if "psite" in df_wt.columns and not df_wt["psite"].isna().all():
- for site in df_wt["psite"].dropna().unique():
- site_wt = df_wt[df_wt["psite"] == site]
- site_ko = df_ko[df_ko["psite"] == site]
- color = px.colors.qualitative.Plotly[hash(site) % len(px.colors.qualitative.Plotly)]
-
- if not site_wt.empty:
- fig_sites_fine.add_trace(go.Scatter(
- x=site_wt["time"],
- y=site_wt["psite_value"],
- name=f"WT {site}",
- line=dict(dash="dash", color=color)
- ))
- if not site_ko.empty:
- fig_sites_fine.add_trace(go.Scatter(
- x=site_ko["time"],
- y=site_ko["psite_value"],
- name=f"KO {site}",
- line=dict(color=color)
- ))
-
- fig_sites_fine.update_layout(
- title=f"{selected_p} Phospho-site State Dynamics",
- xaxis_title="Time (min)",
- yaxis_title="Phospho-site Level (a.u.)",
- template="plotly_white"
- )
- fig_sites_fine.update_xaxes(type="log")
- st.plotly_chart(fig_sites_fine, use_container_width=True)
- else:
- st.info("No phospho site data available for this protein.")
-
-st.divider()
-
-st.header("Data - Fit Inspector")
-wt_p_data = wt_dfp[wt_dfp["protein"] == selected_p]
-ko_p_data = ko_dfp[ko_dfp["protein"] == selected_p]
-wt_r_data = wt_dfr[wt_dfr["protein"] == selected_p]
-ko_r_data = ko_dfr[ko_dfr["protein"] == selected_p]
-wt_pho_data = wt_pho[wt_pho["protein"] == selected_p]
-ko_pho_data = ko_pho[ko_pho["protein"] == selected_p]
-
-col1, col2, col3 = st.columns(3)
+col1, col2, col3 = st.columns(3)
with col1:
fig_insp_r = go.Figure()
@@ -798,86 +1307,723 @@ def extract_fc_from_Y(Y, idx, t, protein, normalize=True):
yaxis_title="Fold Change", template="plotly_white")
st.plotly_chart(fig_insp_p, use_container_width=True)
-# st.divider()
-# col1, col2 = st.columns(2)
-#
-# # --- Signaling Drive Panel (Phosphorylation S)
-# with col1:
-# if selected_p in idx.p2i:
-# p_idx = idx.p2i[selected_p]
-#
-# # Pre-filter WT S once for the selected protein (faster than filtering per-site)
-# wt_s = s_rates.loc[s_rates["protein"].astype(str).str.strip() == selected_p, ["psite", "time", "S"]].copy()
-# wt_s["psite"] = wt_s["psite"].astype(str).str.strip()
-#
-# # Simulate KO S
-# kin_vals_ko = Y_ko[:, [idx.k2i[k] for k in idx.kinases]].T
-# kin_scaled_ko = kin_vals_ko * ko_params["c_k"][:, None]
-# S_ko = sys.W_global @ kin_scaled_ko
-#
-# # Get site indices for the selected protein
-# site_names, site_rows = [], []
-# site_counter = 0
-# for i, p in enumerate(idx.proteins):
-# for j, site in enumerate(idx.sites[i]):
-# if p == selected_p:
-# site_names.append(site)
-# site_rows.append(site_counter)
-# site_counter += 1
-#
-# fig_s_time = go.Figure()
-#
-# for site_name, site_idx in zip(site_names, site_rows):
-# color = px.colors.qualitative.Plotly[hash(site_name) % len(px.colors.qualitative.Plotly)]
-#
-# wt_site = wt_s.loc[wt_s["psite"] == site_name].sort_values("time")
-# if not wt_site.empty:
-# fig_s_time.add_trace(go.Scatter(
-# x=wt_site["time"].to_numpy(),
-# y=wt_site["S"].to_numpy(),
-# name=f"WT {site_name}",
-# line=dict(color=color, dash="dash"),
-# opacity=0.9,
-# ))
-#
-# fig_s_time.add_trace(go.Scatter(
-# x=t_ko,
-# y=S_ko[site_idx, :],
-# name=f"KO {site_name}",
-# line=dict(color=color),
-# opacity=0.9,
-# ))
-#
-# fig_s_time.update_layout(
-# title=f"{selected_p} – Phosphorylation (S)",
-# xaxis_title="Time (min)",
-# yaxis_title="S (Signaling Rate)",
-# template="plotly_white"
-# )
-# st.plotly_chart(fig_s_time, use_container_width=True)
-# else:
-# st.warning(f"{selected_p} not found in protein index.")
-
-
with col3:
if wt_pho_data.empty:
st.info("No phospho-site data available for this protein.")
else:
fig_sites = go.Figure()
- for site in wt_pho_data["psite"].unique():
- color = px.colors.qualitative.Plotly[hash(site) % len(px.colors.qualitative.Plotly)]
+
+ phospho_times = wt_pho_data["time"].dropna().unique()
+
+ if ko_type != "None":
+ ko_phosphosite_inspector_df, ko_phosphosite_debug_df = (
+ _build_inspector_ko_phosphosite_intervention_df(
+ sys_obj=sys,
+ idx=idx,
+ best_params=best_params,
+ ko_params=ko_params,
+ protein=selected_p,
+ wt_pho_data=wt_pho_data,
+ n_points=1000,
+ )
+ )
+ else:
+ ko_phosphosite_inspector_df = pd.DataFrame(
+ columns=["time", "psite", "psite_value"]
+ )
+ ko_phosphosite_debug_df = pd.DataFrame()
+
+ for site in wt_pho_data["psite"].dropna().unique():
+ color = px.colors.qualitative.Plotly[
+ hash(site) % len(px.colors.qualitative.Plotly)
+ ]
+
site_wt = wt_pho_data[wt_pho_data["psite"] == site]
- site_ko = ko_pho_data[ko_pho_data["psite"] == site]
+
fig_sites.add_trace(
- go.Scatter(x=site_wt["time"], y=site_wt["pred_fc"], name=f"WT Site: {site}",
- line=dict(dash="dash", color=color))
+ go.Scatter(
+ x=site_wt["time"],
+ y=site_wt["pred_fc"],
+ name=f"Wild Type {site}",
+ mode="lines",
+ line=dict(dash="dash", color=color, width=2),
+ )
)
- fig_sites.add_trace(
- go.Scatter(x=site_ko["time"], y=site_ko["pred_fc"], name=f"KO Site: {site}", line=dict(color=color))
+
+ if ko_type != "None":
+ site_ko = ko_phosphosite_inspector_df[
+ ko_phosphosite_inspector_df["psite"] == site
+ ]
+
+ if not site_ko.empty:
+ fig_sites.add_trace(
+ go.Scatter(
+ x=site_ko["time"],
+ y=site_ko["psite_value"],
+ name=f"Knockout {site}",
+ mode="lines",
+ line=dict(color=color, width=3),
+ )
+ )
+ else:
+ fig_sites.add_trace(
+ go.Scatter(
+ x=site_wt["time"],
+ y=site_wt["pred_fc"],
+ name=f"Knockout {site}",
+ mode="lines",
+ line=dict(color=color, width=3),
+ )
+ )
+
+ fig_sites.update_layout(
+ title=f"{selected_p} Phosphosite Dynamics",
+ xaxis_title="Time (min)",
+ yaxis_title="Fold Change",
+ template="plotly_white",
+ )
+
+ st.plotly_chart(fig_sites, use_container_width=True)
+
+ if ko_type != "None" and not ko_phosphosite_debug_df.empty:
+ with st.expander("Data inspector phosphosite intervention debug", expanded=False):
+ st.dataframe(
+ ko_phosphosite_debug_df,
+ use_container_width=True,
+ hide_index=True,
+ )
+
+st.divider()
+
+# --- Forward Simulation Panel with Finer Resolution ---
+st.subheader(f"🔁 Forward Simulation of {selected_p}")
+
+t_max = st.slider(
+ "Simulation Time (minutes)",
+ min_value=2,
+ max_value=14 * 24 * 60,
+ value=960,
+ step=60,
+)
+
+n_points = st.select_slider(
+ "Forward simulation resolution",
+ options=[250, 500, 1000, 2500, 5000, 10000],
+ value=1000,
+)
+
+run_forward_panel = st.button(
+ "Run detailed WT/KO forward simulation",
+ key="run-forward-simulation-panel",
+)
+
+forward_cache_key = (
+ selected_p,
+ float(t_max),
+ int(n_points),
+ str(perturbation_signature),
+)
+
+if run_forward_panel:
+ with st.spinner(f"Running WT/KO forward simulation with {n_points} time points..."):
+ forward_results = _run_forward_wt_ko_for_display(
+ sys_obj=sys,
+ best_params=best_params,
+ ko_params=ko_params,
+ t_max=float(t_max),
+ n_points=int(n_points),
+ )
+
+ st.session_state["forward_panel_cache_key"] = forward_cache_key
+ st.session_state["forward_panel_results"] = forward_results
+
+has_forward_results = (
+ st.session_state.get("forward_panel_cache_key") == forward_cache_key
+ and "forward_panel_results" in st.session_state
+)
+
+if not has_forward_results:
+ st.info("Click **Run detailed WT/KO forward simulation** to show the detailed simulation panel.")
+else:
+ cached = st.session_state["forward_panel_results"]
+
+ t_fine = cached["t_fine"]
+ Y_wt = cached["Y_wt"]
+
+ t_ko = cached["t_ko"]
+ Y_ko_raw = cached.get("Y_ko_raw", cached.get("Y_ko"))
+ Y_ko_plot = cached.get("Y_ko_plot", Y_ko_raw)
+
+ used_fixed_y0 = bool(cached.get("used_fixed_y0", False))
+ alignment_mode = cached.get("alignment_mode", "legacy cache / no alignment metadata")
+
+ normalize_forward_states = st.checkbox(
+ "Normalize forward states to t0",
+ value=False,
+ key="normalize_forward_states",
+ )
+
+ normalize_forward_phosphosite_states = st.checkbox(
+ "Normalize forward phosphosite states to WT t0",
+ value=False,
+ key="normalize_forward_phosphosite_states",
+ )
+
+ # Use Y_ko_plot for all displayed KO forward-state panels.
+ # This removes artificial KO t0 inflation while preserving the perturbation trajectory shape.
+ df_wt = extract_fc_from_Y(
+ Y_wt,
+ idx,
+ t_fine,
+ selected_p,
+ normalize=normalize_forward_states,
+ )
+
+ df_ko = extract_fc_from_Y(
+ Y_ko_plot,
+ idx,
+ t_ko,
+ selected_p,
+ normalize=normalize_forward_states,
+ )
+
+ df_wt_phosphosite, raw_wt_phosphosite, plotted_wt_phosphosite = extract_phosphosite_states_from_Y(
+ Y_wt,
+ idx,
+ t_fine,
+ selected_p,
+ normalize_to_t0=normalize_forward_phosphosite_states,
+ reference_raw_states=None,
+ )
+
+ df_ko_phosphosite, raw_ko_phosphosite, plotted_ko_phosphosite = extract_phosphosite_states_from_Y(
+ Y_ko_plot,
+ idx,
+ t_ko,
+ selected_p,
+ normalize_to_t0=normalize_forward_phosphosite_states,
+ reference_raw_states=raw_wt_phosphosite,
+ )
+
+ y_label = (
+ "Phosphosite ODE state normalized to WT t0"
+ if normalize_forward_phosphosite_states
+ else "Raw phosphosite ODE state"
+ )
+
+ def _picked_points_on_displayed_curve(
+ picked_df: pd.DataFrame,
+ curve_df: pd.DataFrame,
+ y_col: str,
+ ) -> pd.DataFrame:
+ """
+ Return picked-data times placed exactly on the displayed WT curve.
+
+ This is for visual alignment only:
+ - x = picked model/data time
+ - y = interpolated value from the plotted WT curve
+ - picked_pred_fc is kept for hover/debug
+ """
+ if picked_df is None or picked_df.empty:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ if curve_df is None or curve_df.empty or y_col not in curve_df.columns:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ picked = picked_df.copy()
+
+ if "time" not in picked.columns or "pred_fc" not in picked.columns:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ picked["time"] = pd.to_numeric(picked["time"], errors="coerce")
+ picked["pred_fc"] = pd.to_numeric(picked["pred_fc"], errors="coerce")
+ picked = picked.dropna(subset=["time", "pred_fc"]).sort_values("time")
+
+ if picked.empty:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ curve = curve_df[["time", y_col]].copy()
+ curve["time"] = pd.to_numeric(curve["time"], errors="coerce")
+ curve[y_col] = pd.to_numeric(curve[y_col], errors="coerce")
+ curve = curve.dropna(subset=["time", y_col]).sort_values("time")
+
+ if curve.empty:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ x_curve = curve["time"].to_numpy(dtype=float)
+ y_curve = curve[y_col].to_numpy(dtype=float)
+
+ # Keep only picked times inside the plotted WT range.
+ picked = picked[
+ (picked["time"] >= float(np.min(x_curve)))
+ & (picked["time"] <= float(np.max(x_curve)))
+ ].copy()
+
+ if picked.empty:
+ return pd.DataFrame(columns=["time", "display_y", "picked_pred_fc"])
+
+ picked["display_y"] = np.interp(
+ picked["time"].to_numpy(dtype=float),
+ x_curve,
+ y_curve,
+ )
+ picked["picked_pred_fc"] = picked["pred_fc"]
+
+ return picked[["time", "display_y", "picked_pred_fc"]]
+
+
+ picked_wt_rna_on_curve = _picked_points_on_displayed_curve(
+ wt_r_data,
+ df_wt,
+ "rna",
+ )
+
+ picked_wt_protein_on_curve = _picked_points_on_displayed_curve(
+ wt_p_data,
+ df_wt,
+ "protein",
+ )
+
+ # --- Plot mRNA and protein
+ col1, col2 = st.columns(2)
+
+ with col1:
+ fig_fine_r = go.Figure()
+ fig_fine_r.add_trace(
+ go.Scatter(
+ x=df_wt["time"],
+ y=df_wt["rna"],
+ name="WT",
+ line=dict(color="black", dash="dash"),
+ )
+ )
+ fig_fine_r.add_trace(
+ go.Scatter(
+ x=df_ko["time"],
+ y=df_ko["rna"],
+ name="KO",
+ line=dict(color="red"),
+ )
+ )
+ if not picked_wt_rna_on_curve.empty:
+ fig_fine_r.add_trace(
+ go.Scatter(
+ x=picked_wt_rna_on_curve["time"],
+ y=picked_wt_rna_on_curve["display_y"],
+ name="Picked WT model",
+ mode="markers",
+ marker=dict(size=6, color="black", symbol="circle-open"),
+ customdata=picked_wt_rna_on_curve[["picked_pred_fc"]],
+ hovertemplate=(
+ "time=%{x}
"
+ "displayed WT y=%{y:.4g}
"
+ "picked pred_fc=%{customdata[0]:.4g}"
+ ""
+ ),
+ showlegend=False,
+ )
+ )
+ fig_fine_r.update_layout(
+ title="mRNA Simulation",
+ xaxis_title="Time",
+ yaxis_title="Fold Change" if normalize_forward_states else "Raw mRNA ODE state",
+ template="plotly_white",
+ )
+ fig_fine_r.update_xaxes(type="log")
+ st.plotly_chart(fig_fine_r, use_container_width=True)
+
+ with col2:
+ fig_fine_p = go.Figure()
+ fig_fine_p.add_trace(
+ go.Scatter(
+ x=df_wt["time"],
+ y=df_wt["protein"],
+ name="WT",
+ line=dict(color="black", dash="dash"),
+ )
+ )
+ fig_fine_p.add_trace(
+ go.Scatter(
+ x=df_ko["time"],
+ y=df_ko["protein"],
+ name="KO",
+ line=dict(color="blue"),
+ )
+ )
+ if not picked_wt_protein_on_curve.empty:
+ fig_fine_p.add_trace(
+ go.Scatter(
+ x=picked_wt_protein_on_curve["time"],
+ y=picked_wt_protein_on_curve["display_y"],
+ name="Picked WT model",
+ mode="markers",
+ marker=dict(size=6, color="black", symbol="circle-open"),
+ customdata=picked_wt_protein_on_curve[["picked_pred_fc"]],
+ hovertemplate=(
+ "time=%{x}
"
+ "displayed WT y=%{y:.4g}
"
+ "picked pred_fc=%{customdata[0]:.4g}"
+ ""
+ ),
+ showlegend=False,
+ )
+ )
+ fig_fine_p.update_layout(
+ title="Protein Simulation",
+ xaxis_title="Time",
+ yaxis_title="Fold Change" if normalize_forward_states else "Raw total protein ODE state",
+ template="plotly_white",
+ )
+ fig_fine_p.update_xaxes(type="log")
+ st.plotly_chart(fig_fine_p, use_container_width=True)
+
+ col1, col2 = st.columns(2)
+
+ # --- Signaling Drive Panel
+ with col1:
+ if selected_p in idx.p2i:
+ site_names, site_rows = [], []
+ site_counter = 0
+
+ for i, p in enumerate(idx.proteins):
+ for site in idx.sites[i]:
+ if p == selected_p:
+ site_names.append(site)
+ site_rows.append(site_counter)
+ site_counter += 1
+
+ fig_s_time = go.Figure()
+
+ # Compute displayed WT and KO signaling drive directly from kinase inputs.
+ # This avoids using state-vector columns as kinase indices.
+ K_wt = np.column_stack(
+ [
+ sys.kin.eval(float(t)) * np.asarray(best_params["c_k"], dtype=float)
+ for t in np.asarray(t_fine, dtype=float)
+ ]
+ )
+ S_wt_t = sys.W_global @ K_wt
+
+ K_ko = np.column_stack(
+ [
+ sys.kin.eval(float(t)) * np.asarray(ko_params["c_k"], dtype=float)
+ for t in np.asarray(t_ko, dtype=float)
+ ]
+ )
+ S_ko_t = sys.W_global @ K_ko
+
+ # Use S_rates only for marker times / hover values.
+ # Marker y-values are interpolated from the displayed WT S curve,
+ # so the spheres sit exactly on the WT line.
+ s_rate_value_col = None
+ s_rate_site_col = None
+
+ if s_rates is not None and not s_rates.empty:
+ for candidate in ("S", "s", "S_rate", "s_rate", "rate", "value", "phospho_drive"):
+ if candidate in s_rates.columns:
+ s_rate_value_col = candidate
+ break
+
+ if s_rate_value_col is None:
+ excluded_cols = {"protein", "psite", "site", "time"}
+ numeric_cols = [
+ c for c in s_rates.columns
+ if c not in excluded_cols and pd.api.types.is_numeric_dtype(s_rates[c])
+ ]
+ s_rate_value_col = numeric_cols[0] if numeric_cols else None
+
+ if "psite" in s_rates.columns:
+ s_rate_site_col = "psite"
+ elif "site" in s_rates.columns:
+ s_rate_site_col = "site"
+
+ for site_name, site_idx in zip(site_names, site_rows):
+ color = px.colors.qualitative.Plotly[
+ hash(site_name) % len(px.colors.qualitative.Plotly)
+ ]
+
+ wt_s_curve = np.asarray(S_wt_t[site_idx, :], dtype=float)
+ ko_s_curve = np.asarray(S_ko_t[site_idx, :], dtype=float)
+
+ fig_s_time.add_trace(
+ go.Scatter(
+ x=t_fine,
+ y=wt_s_curve,
+ name=f"WT S {site_name}",
+ mode="lines",
+ line=dict(dash="dash", color=color, width=2),
+ opacity=0.9,
+ )
+ )
+
+ fig_s_time.add_trace(
+ go.Scatter(
+ x=t_ko,
+ y=ko_s_curve,
+ name=f"KO S {site_name}",
+ mode="lines",
+ line=dict(dash="solid", color=color, width=3),
+ opacity=0.9,
+ )
+ )
+
+ if s_rates is not None and not s_rates.empty and "time" in s_rates.columns:
+ s_site = s_rates.copy()
+
+ if "protein" in s_site.columns:
+ s_site = s_site[
+ s_site["protein"].astype(str).str.strip().str.upper()
+ == selected_p.upper()
+ ]
+
+ if s_rate_site_col is not None:
+ s_site = s_site[
+ s_site[s_rate_site_col].astype(str).str.strip()
+ == str(site_name)
+ ]
+
+ if not s_site.empty:
+ s_site["time"] = pd.to_numeric(s_site["time"], errors="coerce")
+ s_site = s_site.dropna(subset=["time"]).sort_values("time")
+
+ # Keep marker times inside the displayed WT S range.
+ s_site = s_site[
+ (s_site["time"] >= float(np.min(t_fine)))
+ & (s_site["time"] <= float(np.max(t_fine)))
+ ].copy()
+
+ if not s_site.empty:
+ s_marker_y = np.interp(
+ s_site["time"].to_numpy(dtype=float),
+ np.asarray(t_fine, dtype=float),
+ wt_s_curve,
+ )
+
+ custom_cols = []
+ if s_rate_value_col is not None and s_rate_value_col in s_site.columns:
+ s_site[s_rate_value_col] = pd.to_numeric(
+ s_site[s_rate_value_col],
+ errors="coerce",
+ )
+ custom_cols = [s_rate_value_col]
+
+ fig_s_time.add_trace(
+ go.Scatter(
+ x=s_site["time"],
+ y=s_marker_y,
+ name=f"Picked WT S {site_name}",
+ mode="markers",
+ marker=dict(size=6, color=color, symbol="circle-open"),
+ customdata=s_site[custom_cols] if custom_cols else None,
+ hovertemplate=(
+ "time=%{x}
"
+ "displayed WT S=%{y:.4g}
"
+ + (
+ f"picked {s_rate_value_col}=%{{customdata[0]:.4g}}
"
+ if custom_cols else ""
+ )
+ + ""
+ ),
+ showlegend=False,
+ )
+ )
+
+ fig_s_time.update_layout(
+ title=f"{selected_p} – Phosphorylation (S)",
+ xaxis_title="Time (min)",
+ yaxis_title="S (Signaling Rate)",
+ template="plotly_white",
+ )
+ fig_s_time.update_xaxes(type="log")
+ st.plotly_chart(fig_s_time, use_container_width=True)
+ else:
+ st.warning(f"{selected_p} not found in protein index.")
+
+ # --- Phosphosite calibrated fine-grid ODE dynamics panel
+ with col2:
+ fig_sites_fine = go.Figure()
+
+ wt_pho_forward_data = wt_pho[wt_pho["protein"] == selected_p].copy()
+
+ if not df_wt_phosphosite.empty and not wt_pho_forward_data.empty:
+ p_idx = idx.p2i[selected_p]
+ site_order = list(idx.sites[p_idx])
+
+ raw_wt_matrix = np.asarray(raw_wt_phosphosite, dtype=float)
+ raw_ko_matrix = np.asarray(raw_ko_phosphosite, dtype=float)
+
+ t_wt_grid = np.asarray(t_fine, dtype=float)
+ t_ko_grid = np.asarray(t_ko, dtype=float)
+
+ eps = 1e-12
+ debug_rows = []
+
+ for j, site in enumerate(site_order):
+ site_wt_picked = wt_pho_forward_data[
+ wt_pho_forward_data["psite"].astype(str) == str(site)
+ ].copy()
+
+ if site_wt_picked.empty:
+ continue
+
+ site_wt_picked["time"] = pd.to_numeric(
+ site_wt_picked["time"], errors="coerce"
+ )
+ site_wt_picked["pred_fc"] = pd.to_numeric(
+ site_wt_picked["pred_fc"], errors="coerce"
+ )
+ site_wt_picked = site_wt_picked.dropna(subset=["time", "pred_fc"])
+ site_wt_picked = site_wt_picked.sort_values("time")
+
+ if site_wt_picked.empty:
+ continue
+
+ picked_t = site_wt_picked["time"].to_numpy(dtype=float)
+ picked_fc = site_wt_picked["pred_fc"].to_numpy(dtype=float)
+
+ wt_raw_fine = raw_wt_matrix[:, j]
+ ko_raw_fine = raw_ko_matrix[:, j]
+
+ # Interpolate only the smooth raw WT ODE state to picked model times
+ # for estimating one scalar measurement calibration. This does NOT
+ # create a time-dependent coarse calibration curve.
+ wt_raw_at_picked_t = np.interp(
+ picked_t,
+ t_wt_grid,
+ wt_raw_fine,
+ )
+
+ valid = (
+ np.isfinite(wt_raw_at_picked_t)
+ & np.isfinite(picked_fc)
+ & (np.abs(wt_raw_at_picked_t) > eps)
+ )
+
+ if np.any(valid):
+ # Robust scalar calibration: median picked_FC / raw_state.
+ # This puts the fine ODE trajectory on the same magnitude scale
+ # as the pipeline-picked phosphosite prediction without using
+ # tiny t0 denominators or piecewise interpolation.
+ ratios = picked_fc[valid] / wt_raw_at_picked_t[valid]
+ ratios = ratios[np.isfinite(ratios)]
+
+ if ratios.size:
+ calibration = float(np.median(ratios))
+ else:
+ calibration = 1.0
+ else:
+ calibration = 1.0
+
+ wt_fc_fine = wt_raw_fine * calibration
+
+ if ko_type != "None":
+ ko_fc_fine = ko_raw_fine * calibration
+ else:
+ ko_fc_fine = wt_fc_fine.copy()
+
+ color = px.colors.qualitative.Plotly[
+ hash(site) % len(px.colors.qualitative.Plotly)
+ ]
+
+ fig_sites_fine.add_trace(
+ go.Scatter(
+ x=t_wt_grid,
+ y=wt_fc_fine,
+ name=f"Wild Type {site}",
+ mode="lines",
+ line=dict(dash="dash", color=color, width=2),
+ )
+ )
+
+ fig_sites_fine.add_trace(
+ go.Scatter(
+ x=t_ko_grid,
+ y=ko_fc_fine,
+ name=f"Knockout {site}",
+ mode="lines",
+ line=dict(color=color, width=3),
+ )
+ )
+
+ # Optional picked WT model points, useful for checking alignment.
+ fig_sites_fine.add_trace(
+ go.Scatter(
+ x=picked_t,
+ y=picked_fc,
+ name=f"Picked WT model {site}",
+ mode="markers",
+ marker=dict(size=6, color=color, symbol="circle-open"),
+ showlegend=False,
+ )
+ )
+
+ debug_rows.append(
+ {
+ "psite": site,
+ "WT raw min": float(np.nanmin(wt_raw_fine)),
+ "WT raw max": float(np.nanmax(wt_raw_fine)),
+ "KO raw min": float(np.nanmin(ko_raw_fine)),
+ "KO raw max": float(np.nanmax(ko_raw_fine)),
+ "WT calibrated FC min": float(np.nanmin(wt_fc_fine)),
+ "WT calibrated FC max": float(np.nanmax(wt_fc_fine)),
+ "KO calibrated FC min": float(np.nanmin(ko_fc_fine)),
+ "KO calibrated FC max": float(np.nanmax(ko_fc_fine)),
+ "picked WT FC min": float(np.nanmin(picked_fc)),
+ "picked WT FC max": float(np.nanmax(picked_fc)),
+ "scalar calibration": calibration,
+ }
+ )
+
+ fig_sites_fine.update_layout(
+ title=f"{selected_p} Phosphosite Dynamics",
+ xaxis_title="Time (min)",
+ yaxis_title="Fold Change",
+ template="plotly_white",
)
- fig_sites.update_layout(title=f"{selected_p} Phospho-site Dynamics", xaxis_title="Time (min)",
- yaxis_title="Fold Change", template="plotly_white")
- st.plotly_chart(fig_sites, use_container_width=True)
+ fig_sites_fine.update_xaxes(type="log")
+ st.plotly_chart(fig_sites_fine, use_container_width=True)
+
+ if raw_wt_matrix.shape == raw_ko_matrix.shape and raw_wt_matrix.size:
+ phosphosite_t0_max_abs_diff = float(
+ np.max(np.abs(raw_wt_matrix[0, :] - raw_ko_matrix[0, :]))
+ )
+ else:
+ phosphosite_t0_max_abs_diff = float("nan")
+
+ with st.expander("Forward phosphosite calibrated dynamics debug", expanded=False):
+ st.write(
+ {
+ "alignment_mode": alignment_mode,
+ "used_fixed_y0_argument": used_fixed_y0,
+ "max_abs_t0_difference_WT_vs_displayed_KO_phosphosite": phosphosite_t0_max_abs_diff,
+ "calibration_mode": "single robust per-site scalar: median(picked WT FC / WT raw ODE state)",
+ }
+ )
+
+ if debug_rows:
+ st.dataframe(
+ pd.DataFrame(debug_rows),
+ use_container_width=True,
+ hide_index=True,
+ )
+
+ if Y_ko_raw is not None:
+ _, raw_ko_unaligned_phosphosite, _ = extract_phosphosite_states_from_Y(
+ Y_ko_raw,
+ idx,
+ t_ko,
+ selected_p,
+ normalize_to_t0=False,
+ )
+ unaligned_min, unaligned_max = _safe_min_max(raw_ko_unaligned_phosphosite)
+ st.write(
+ {
+ "raw_unaligned_KO_phosphosite_min": unaligned_min,
+ "raw_unaligned_KO_phosphosite_max": unaligned_max,
+ }
+ )
+ else:
+ st.info("No phospho site data available for this protein.")
st.divider()
@@ -899,7 +2045,7 @@ def extract_fc_from_Y(Y, idx, t, protein, normalize=True):
current_layer_nodes = {target}
processed_nodes = set()
- final_kin_act = sys.kin.Kmat[:, -1] * sys.c_k
+ final_kin_act = sys.kin.Kmat[:, -1] * ko_params["c_k"]
S_final = sys.W_global.dot(final_kin_act)
for d in range(depth):
@@ -907,8 +2053,7 @@ def extract_fc_from_Y(Y, idx, t, protein, normalize=True):
if not current_layer_nodes:
break
- if ko_type == "Kinase (Activity)" or d > 0:
- restore_Kinase()
+ if any(k in idx.kinases for k in current_layer_nodes):
for k_name in current_layer_nodes:
if k_name in idx.kinases:
k_idx = idx.k2i[k_name]
@@ -1575,7 +2720,7 @@ def _functional_influence_edges(mode: str, seed: str, depth: int, t_eval: float,
depth (int): Maximum depth of propagation in the network.
t_eval ("""
# WT
- sys_wt, idx_wt, _, df_tf_wt, _ = load_system()
+ sys_wt, idx_wt, _, df_tf_wt, _ = load_system(INTENDED_MODEL)
df_wt = _cascade_edges_from_seed(
sys_wt, idx_wt, best_params, df_tf_wt,
seed=seed, depth=depth, t_eval=t_eval,
@@ -1583,7 +2728,7 @@ def _functional_influence_edges(mode: str, seed: str, depth: int, t_eval: float,
)
# KO
- sys_ko, idx_ko, _, df_tf_ko, _ = load_system()
+ sys_ko, idx_ko, _, df_tf_ko, _ = load_system(INTENDED_MODEL)
df_ko = _cascade_edges_from_seed(
sys_ko, idx_ko, ko_params, df_tf_ko,
seed=seed, depth=depth, t_eval=t_eval,
@@ -1693,7 +2838,7 @@ def build_network_from_params(params):
params: Parameters required to build the network.
"""
# HARD RESET
- sys_local, idx_local, _, _, _ = load_system()
+ sys_local, idx_local, _, _, _ = load_system(INTENDED_MODEL)
return _build_global_edge_tables(
sys_local, idx_local, params, df_tf_model, t_eval=float(t_eval)
)
@@ -1840,9 +2985,6 @@ def _prepare_edges_for_mode(mode: str):
"or the browser will become heavy."
)
-from networkmodel.simulate import simulate_diffrax
-
-
def _compute_state_snapshot_sweep(sys: System, idx: Index, params: dict, t_eval: float):
"""
Computes the state snapshot for a system over a specified time range.
@@ -1877,7 +3019,7 @@ def _compute_state_snapshot_sweep(sys: System, idx: Index, params: dict, t_eval:
# Diffrax requires strictly increasing values for this comparison grid
t_grid.sort()
- Y = simulate_diffrax(sys, t_grid, rtol=1e-6, atol=1e-8, max_steps=50000)
+ Y = simulate_mod.simulate_diffrax(sys, t_grid, rtol=1e-6, atol=1e-8, max_steps=50000)
y_last = np.asarray(Y[-1], dtype=float)
Kt = sys.kin.eval(t_eval) * sys.c_k
@@ -1978,7 +3120,7 @@ def build_network_from_params_at_time(params, t_eval_local: float):
The resulting global edge tables after applying the parameters at the
specified time.
"""
- sys_local, idx_local, _, _, _ = load_system()
+ sys_local, idx_local, _, _, _ = load_system(INTENDED_MODEL)
return _build_global_edge_tables_at_time_sweep(
sys_local, idx_local, params, df_tf_model, t_eval=float(t_eval_local)
)
@@ -2094,810 +3236,1122 @@ def _prepare_edges_for_mode_at_time(mode: str, t_eval_local: float):
key="sweep_edges_csv",
)
-# # =========================
-# # TIME-RESOLVED ANIMATED NETWORK (pre-steady state)
-# # =========================
-
-# # -------------------------
-# # SAFE animation export (no kaleido hard-fail)
-# # -------------------------
-# def _export_plotly_animation(fig: go.Figure, fmt: str = "gif", fps: int = 6, scale: int = 2) -> bytes:
-# """
-# Render Plotly animation frames -> GIF/MP4 bytes.
-#
-# Preferred: Plotly+kaleido (fig.to_image).
-# Fallback: raises a clean RuntimeError with actionable instructions.
-#
-# Notes:
-# - GIF: uses imageio.mimsave
-# - MP4: requires ffmpeg available to imageio (imageio-ffmpeg)
-# """
-# frames = list(fig.frames or [])
-# if not frames:
-# raise RuntimeError("No frames found in the figure. Nothing to export.")
-#
-# images = []
-# for fr in frames:
-# f = go.Figure(data=fr.data, layout=fig.layout)
-# try:
-# png_bytes = f.to_image(format="png", scale=scale, engine="kaleido")
-# except Exception as e:
-# raise RuntimeError(
-# "Export requires Plotly image export support (kaleido). "
-# "If kaleido is installed but Plotly can't see it, ensure you are installing it "
-# "inside the SAME environment where Streamlit runs.\n"
-# "Conda: conda install -c conda-forge python-kaleido\n"
-# "Pip: pip install -U kaleido\n"
-# f"Original error: {repr(e)}"
-# )
-# images.append(imageio.imread(png_bytes))
-#
-# buf = io.BytesIO()
-#
-# fmt_l = fmt.lower()
-# if fmt_l == "gif":
-# imageio.mimsave(buf, images, format="GIF", fps=fps)
-# return buf.getvalue()
-#
-# if fmt_l == "mp4":
-# # imageio writes mp4 via ffmpeg
-# writer = imageio.get_writer(buf, format="FFMPEG", mode="I", fps=fps, codec="libx264")
-# for im in images:
-# writer.append_data(im)
-# writer.close()
-# return buf.getvalue()
-#
-# raise ValueError("fmt must be 'gif' or 'mp4'")
-#
-#
-# # -------------------------
-# # State extractors
-# # -------------------------
-# def _extract_mrna_vec_from_y(y_row: np.ndarray, idx: Index) -> np.ndarray:
-# out = np.zeros(len(idx.proteins), dtype=float)
-# for i in range(len(idx.proteins)):
-# st_y = idx.offset_y[i]
-# out[i] = float(y_row[st_y + 0])
-# return out
-#
-#
-# def _extract_total_protein_vec_from_y(y_row: np.ndarray, idx: Index) -> np.ndarray:
-# out = np.zeros(len(idx.proteins), dtype=float)
-# for i in range(len(idx.proteins)):
-# st_y = idx.offset_y[i]
-# prot = float(y_row[st_y + 1])
-# ns = int(idx.n_sites[i])
-# if ns > 0:
-# phos_sum = float(np.sum(y_row[st_y + 2 : st_y + 2 + ns]))
-# out[i] = prot + phos_sum
-# else:
-# out[i] = prot
-# return out
-#
-#
-# def _delta_log2_fc(ko: np.ndarray, wt: np.ndarray, eps: float = 1e-9) -> np.ndarray:
-# """log2(KO/WT)"""
-# return np.log2((ko + eps) / (wt + eps))
-#
-#
-# def _total_protein_from_Y_row(y_row: np.ndarray, idx: Index, protein: str) -> float:
-# p_i = idx.p2i[protein]
-# st_y = idx.offset_y[p_i]
-# prot = float(y_row[st_y + 1])
-# ns = int(idx.n_sites[p_i])
-# if ns > 0:
-# return prot + float(np.sum(y_row[st_y + 2 : st_y + 2 + ns]))
-# return prot
-#
-#
-# # -------------------------
-# # Edges at time t
-# # -------------------------
-# def _edge_tables_at_time(
-# sys: System,
-# idx: Index,
-# df_tf_model: pd.DataFrame | None,
-# y_row: np.ndarray,
-# t: float,
-# ) -> tuple[pd.DataFrame, pd.DataFrame]:
-# """
-# signaling: kinase -> protein, weight = Σ_sites beta(site,k) * Kt_k(t)
-# transcription: tf -> target, weight = tf_scale * tf_mat[target,tf] * TF_total(t)
-# """
-# # --- signaling ---
-# Kt = sys.kin.eval(float(t)) * sys.c_k # (nK,)
-# W = sys.W_global.tocoo()
-# edge_contrib = W.data * Kt[W.col] # per-site contribution
-#
-# prot_idx = np.searchsorted(idx.offset_s, W.row, side="right") - 1
-# prot_idx = np.clip(prot_idx, 0, len(idx.proteins) - 1)
-#
-# df_sig = pd.DataFrame(
-# {
-# "src": np.asarray(idx.kinases, dtype=object)[W.col],
-# "tgt": np.asarray(idx.proteins, dtype=object)[prot_idx],
-# "weight": edge_contrib.astype(float),
-# }
-# )
-# df_sig = df_sig.groupby(["src", "tgt"], as_index=False).agg(weight=("weight", "sum"))
-# df_sig["type"] = "signaling"
-#
-# # --- transcription ---
-# df_tf_edges = pd.DataFrame(columns=["src", "tgt", "weight", "type"])
-# if df_tf_model is not None and not df_tf_model.empty:
-# tf_mat = sys.tf_mat
-# tf_scale = float(getattr(sys, "tf_scale", 1.0))
-#
-# rows = []
-# for r in df_tf_model.itertuples(index=False):
-# tf = getattr(r, "tf")
-# tgt = getattr(r, "target")
-# if tf not in idx.p2i or tgt not in idx.p2i:
-# continue
-#
-# i_tgt = idx.p2i[tgt]
-# j_tf = idx.p2i[tf]
-#
-# try:
-# coeff = float(tf_mat[i_tgt, j_tf])
-# except Exception:
-# coeff = float(np.asarray(tf_mat[i_tgt, j_tf]).squeeze())
-#
-# if abs(coeff) < 1e-14:
-# continue
-#
-# tf_level = _total_protein_from_Y_row(y_row, idx, tf)
-# drive = tf_scale * coeff * tf_level
-# rows.append((tf, tgt, float(drive)))
-#
-# if rows:
-# df_tf_edges = pd.DataFrame(rows, columns=["src", "tgt", "weight"])
-# df_tf_edges["type"] = "transcription"
-#
-# return df_sig, df_tf_edges
-#
-#
-# def _filter_edges(
-# df_sig: pd.DataFrame,
-# df_tf: pd.DataFrame,
-# include_tf: bool,
-# min_abs_w: float,
-# top_k: int,
-# edge_scale: float = 1.0,
-# ) -> pd.DataFrame:
-# df_all = df_sig.copy()
-# if include_tf and df_tf is not None and not df_tf.empty:
-# df_all = pd.concat([df_all, df_tf], ignore_index=True)
-#
-# if df_all.empty:
-# return df_all
-#
-# df_all["weight"] = pd.to_numeric(df_all["weight"], errors="coerce") * float(edge_scale)
-# df_all = df_all.replace([np.inf, -np.inf], np.nan).dropna(subset=["src", "tgt", "weight"])
-#
-# df_all["absw"] = df_all["weight"].abs()
-# df_all = df_all[df_all["absw"] >= float(min_abs_w)].copy()
-# df_all = df_all.sort_values("absw", ascending=False).head(int(top_k)).copy()
-# return df_all
-#
-#
-# def _build_union_graph(edge_frames: list[pd.DataFrame]) -> nx.DiGraph:
-# G = nx.DiGraph()
-# for df in edge_frames:
-# if df is None or df.empty:
-# continue
-# for r in df.itertuples(index=False):
-# G.add_edge(r.src, r.tgt, etype=r.type)
-# return G
-#
-#
-# def _node_activity_frames(edge_frames: list[pd.DataFrame], nodes: list[str]) -> list[np.ndarray]:
-# node2i = {n: i for i, n in enumerate(nodes)}
-# frames = []
-# for df in edge_frames:
-# active = np.zeros(len(nodes), dtype=float)
-# if df is not None and not df.empty:
-# act_nodes = pd.unique(pd.concat([df["src"], df["tgt"]], ignore_index=True))
-# for n in act_nodes:
-# j = node2i.get(n)
-# if j is not None:
-# active[j] = 1.0
-# frames.append(active)
-# return frames
-#
-#
-# def _aligned_node_colors_for_frame(
-# nodes: list[str],
-# idx: Index,
-# wt_vec: np.ndarray,
-# ko_vec: np.ndarray,
-# eps: float = 1e-9,
-# ) -> np.ndarray:
-# """
-# Returns log2(KO/WT) aligned to `nodes`.
-# Unknown nodes (e.g., kinases not in idx.p2i) get NaN.
-# """
-# out = np.full(len(nodes), np.nan, dtype=float)
-# for i, n in enumerate(nodes):
-# if n in idx.p2i:
-# j = idx.p2i[n]
-# out[i] = float(np.log2((float(ko_vec[j]) + eps) / (float(wt_vec[j]) + eps)))
-# return out
-#
-#
-# # -------------------------
-# # Plotly animated network (fixed: no array line widths; aligned node colors)
-# # -------------------------
-# def _plotly_animated_network(
-# edge_frames: list[pd.DataFrame],
-# times: np.ndarray,
-# title: str,
-# node_color_frames: list[np.ndarray] | None = None,
-# node_cmax: float = 2.0,
-# highlight_nodes: bool = True,
-# ) -> go.Figure:
-# G = _build_union_graph(edge_frames)
-# if G.number_of_nodes() == 0:
-# return go.Figure()
-#
-# pos = nx.spring_layout(G, k=0.8, seed=42)
-# nodes = list(G.nodes())
-#
-# activity_frames = _node_activity_frames(edge_frames, nodes) if highlight_nodes else None
-#
-# node_x = [pos[n][0] for n in nodes]
-# node_y = [pos[n][1] for n in nodes]
-#
-# base_size = 10.0
-# boost_size = 18.0
-#
-# init_act = activity_frames[0] if activity_frames is not None else np.ones(len(nodes), dtype=float)
-# init_sizes = base_size + boost_size * init_act
-#
-# init_colors = None
-# if node_color_frames is not None and len(node_color_frames) > 0:
-# init_colors = node_color_frames[0]
-#
-# # IMPORTANT: Plotly does NOT support array-valued marker.line.width. Keep it scalar.
-# node_trace = go.Scatter(
-# x=node_x,
-# y=node_y,
-# mode="markers+text",
-# text=nodes,
-# textposition="top center",
-# hoverinfo="text",
-# marker=dict(
-# size=init_sizes, # array OK
-# line=dict(width=1, color="black"),
-# color=init_colors if init_colors is not None else None,
-# colorscale="RdBu_r",
-# cmin=-float(node_cmax),
-# cmax=float(node_cmax),
-# colorbar=dict(title="log2(KO/WT)", thickness=15) if init_colors is not None else None,
-# ),
-# showlegend=False,
-# )
-#
-# union_edges = list(G.edges())
-# edge_meta = {(u, v): G.edges[u, v].get("etype", "signaling") for (u, v) in union_edges}
-#
-# def _edge_traces_for_frame(df_edges: pd.DataFrame):
-# if df_edges is None or df_edges.empty:
-# return [
-# go.Scatter(x=[], y=[], mode="lines", line=dict(width=1, color="#3498db"), opacity=0.2, hoverinfo="none"),
-# go.Scatter(x=[], y=[], mode="lines", line=dict(width=1, color="#e67e22"), opacity=0.2, hoverinfo="none"),
-# ]
-#
-# wmap = {(r.src, r.tgt): float(r.weight) for r in df_edges.itertuples(index=False)}
-#
-# # Build per-type traces (performance-first)
-# traces = []
-# for etype, color in [("signaling", "#3498db"), ("transcription", "#e67e22")]:
-# x_e, y_e, widths = [], [], []
-# for (u, v) in union_edges:
-# if edge_meta[(u, v)] != etype:
-# continue
-# w = wmap.get((u, v), 0.0)
-# if abs(w) <= 0:
-# continue
-# x0, y0 = pos[u]
-# x1, y1 = pos[v]
-# x_e += [x0, x1, None]
-# y_e += [y0, y1, None]
-# widths.append(float(np.clip(np.log10(1.0 + abs(w)) * 3.0, 0.2, 6.0)))
-#
-# w_med = float(np.median(widths)) if widths else 0.2
-# traces.append(
-# go.Scatter(
-# x=x_e,
-# y=y_e,
-# mode="lines",
-# line=dict(width=w_med, color=color),
-# opacity=0.55,
-# hoverinfo="none",
-# showlegend=False,
-# )
-# )
-# return traces
-#
-# init_edge_traces = _edge_traces_for_frame(edge_frames[0])
-#
-# frames = []
-# for i, (df_e, t) in enumerate(zip(edge_frames, times)):
-# edge_traces_i = _edge_traces_for_frame(df_e)
-#
-# if activity_frames is not None:
-# act = activity_frames[i]
-# sizes_i = base_size + boost_size * act
-# else:
-# sizes_i = base_size
-#
-# if node_color_frames is not None:
-# colors_i = node_color_frames[i]
-# node_trace_i = go.Scatter(
-# x=node_x,
-# y=node_y,
-# mode="markers+text",
-# text=nodes,
-# textposition="top center",
-# hoverinfo="text",
-# marker=dict(
-# size=sizes_i if np.iterable(sizes_i) else float(sizes_i),
-# line=dict(width=1, color="black"),
-# color=colors_i,
-# colorscale="RdBu_r",
-# cmin=-float(node_cmax),
-# cmax=float(node_cmax),
-# ),
-# showlegend=False,
-# )
-# else:
-# node_trace_i = go.Scatter(
-# x=node_x, y=node_y,
-# mode="markers+text",
-# text=nodes,
-# textposition="top center",
-# hoverinfo="text",
-# marker=dict(size=sizes_i if np.iterable(sizes_i) else float(sizes_i), line=dict(width=1, color="black")),
-# showlegend=False,
-# )
-#
-# frames.append(
-# go.Frame(
-# data=edge_traces_i + [node_trace_i],
-# name=str(i),
-# layout=go.Layout(title=f"{title} (t={float(t):.1f} min)"),
-# )
-# )
-#
-# fig = go.Figure(
-# data=init_edge_traces + [node_trace],
-# layout=go.Layout(
-# title=f"{title} (t={float(times[0]):.1f} min)",
-# hovermode="closest",
-# margin=dict(b=0, l=0, r=0, t=50),
-# xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
-# yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
-# template="plotly_white",
-# updatemenus=[
-# dict(
-# type="buttons",
-# showactive=False,
-# buttons=[
-# dict(label="Play", method="animate",
-# args=[None, dict(frame=dict(duration=250, redraw=True), fromcurrent=True)]),
-# dict(label="Pause", method="animate",
-# args=[[None], dict(frame=dict(duration=0, redraw=False), mode="immediate")]),
-# ],
-# )
-# ],
-# sliders=[
-# dict(
-# active=0,
-# currentvalue=dict(prefix="Frame: "),
-# steps=[
-# dict(method="animate",
-# args=[[str(i)], dict(mode="immediate", frame=dict(duration=0, redraw=True))],
-# label=f"{float(t):.0f}")
-# for i, t in enumerate(times)
-# ],
-# )
-# ],
-# ),
-# frames=frames,
-# )
-# return fig
-#
-#
-# # =========================
-# # STREAMLIT PANEL
-# # =========================
-# st.divider()
-# st.header("🕸️ Time-Resolved Animated Network (pre-steady-state)")
-#
-# c1, c2, c3, c4 = st.columns(4)
-# with c1:
-# view_mode = st.selectbox("View", ["WT", "KO", "Δ(KO−WT)"], index=1, key="anim_net_view_mode")
-# with c2:
-# t_end = st.slider("End time (min)", min_value=120, max_value=24 * 7 * 60, value=120, step=100, key="anim_net_t_end")
-# with c3:
-# n_frames = st.slider("Frames", min_value=10, max_value=1000, value=30, step=5, key="anim_net_n_frames")
-# with c4:
-# include_tf_edges = st.checkbox("Include TF edges", value=True, key="anim_net_include_tf_edges")
-#
-# min_abs_w = st.number_input("Min |weight| filter", min_value=0.0, value=0.001, step=0.001,
-# format="%.4f", key="anim_net_min_abs_w")
-#
-# top_k = st.slider("Top edges per frame", min_value=50, max_value=800, value=250, step=50, key="anim_net_top_k")
-#
-# node_metric = st.selectbox("Node color metric", ["None", "ΔmRNA (log2 KO/WT)", "ΔProtein (log2 KO/WT)"],
-# index=2, key="anim_net_node_metric")
-#
-# node_cmax = st.slider("Node color range (± log2)", min_value=1.1, max_value=10.0, value=2.0, step=0.1,
-# key="anim_net_node_cmax")
-#
-# edge_scale = st.slider("Edge weight scaling", min_value=0.1, max_value=100.0, value=1.0, step=0.1,
-# key="anim_net_edge_scale")
-#
-#
-# def _simulate_state_series(params: dict, t_end: float, n_points: int):
-# sys_local, idx_local, _, df_tf_local, _ = load_system()
-# sys_local.update(**params)
-#
-# t_grid = np.linspace(0.0, float(t_end), int(n_points))
-# t_sim, Y = simulate_until_steady(sys_local, t_max=float(t_end), n_points=int(n_points))
-#
-# if len(t_sim) != len(t_grid):
-# Yg = np.empty((len(t_grid), Y.shape[1]), dtype=float)
-# for j in range(Y.shape[1]):
-# Yg[:, j] = np.interp(t_grid, t_sim, Y[:, j])
-# Y = Yg
-# t_sim = t_grid
-#
-# return sys_local, idx_local, df_tf_local, t_sim, Y
-#
-#
-# # trajectories
-# sys_wt, idx_wt, df_tf_wt, t_wt, Y_wt = _simulate_state_series(best_params, t_end=float(t_end), n_points=int(n_frames))
-# sys_ko, idx_ko, df_tf_ko, t_ko, Y_ko = _simulate_state_series(ko_params, t_end=float(t_end), n_points=int(n_frames))
-#
-# # edge frames
-# edge_frames: list[pd.DataFrame] = []
-# times = t_wt # same grid
-#
-# for i, t in enumerate(times):
-# df_sig_wt, df_tf_e_wt = _edge_tables_at_time(sys_wt, idx_wt, df_tf_wt, Y_wt[i], t)
-# df_sig_ko, df_tf_e_ko = _edge_tables_at_time(sys_ko, idx_ko, df_tf_ko, Y_ko[i], t)
-#
-# if view_mode == "WT":
-# df_e = _filter_edges(df_sig_wt, df_tf_e_wt, include_tf_edges, min_abs_w, top_k, edge_scale)
-# elif view_mode == "KO":
-# df_e = _filter_edges(df_sig_ko, df_tf_e_ko, include_tf_edges, min_abs_w, top_k, edge_scale)
-# else:
-# # Δ(KO−WT)
-# df_w = pd.concat([df_sig_wt, df_tf_e_wt], ignore_index=True)
-# df_k = pd.concat([df_sig_ko, df_tf_e_ko], ignore_index=True)
-# key = ["src", "tgt", "type"]
-#
-# df_w = df_w[key + ["weight"]].rename(columns={"weight": "w_wt"})
-# df_k = df_k[key + ["weight"]].rename(columns={"weight": "w_ko"})
-#
-# df_d = df_k.merge(df_w, on=key, how="outer")
-# df_d["w_ko"] = df_d["w_ko"].fillna(0.0)
-# df_d["w_wt"] = df_d["w_wt"].fillna(0.0)
-# df_d["weight"] = df_d["w_ko"] - df_d["w_wt"]
-# df_d = df_d[key + ["weight"]]
-#
-# df_sig_d = df_d[df_d["type"] == "signaling"].copy()
-# df_tf_d = df_d[df_d["type"] == "transcription"].copy()
-#
-# df_e = _filter_edges(df_sig_d, df_tf_d, include_tf_edges, min_abs_w, top_k, edge_scale)
-#
-# edge_frames.append(df_e)
-#
-# # build union node list for alignment
-# G_union = _build_union_graph(edge_frames)
-# nodes_union = list(G_union.nodes())
-#
-# # aligned node colors per frame
-# node_color_frames = None
-# if node_metric != "None" and len(nodes_union) > 0:
-# node_color_frames = []
-# for i in range(len(times)):
-# if "mRNA" in node_metric:
-# wt_vec = _extract_mrna_vec_from_y(Y_wt[i], idx_wt)
-# ko_vec = _extract_mrna_vec_from_y(Y_ko[i], idx_ko)
-# else:
-# wt_vec = _extract_total_protein_vec_from_y(Y_wt[i], idx_wt)
-# ko_vec = _extract_total_protein_vec_from_y(Y_ko[i], idx_ko)
-#
-# node_color_frames.append(_aligned_node_colors_for_frame(nodes_union, idx_wt, wt_vec, ko_vec))
-#
-# # edge dynamics long table
-# edge_long = []
-# for i, t in enumerate(times):
-# df = edge_frames[i].copy()
-# if df.empty:
-# continue
-# df["time"] = float(t)
-# edge_long.append(df)
-#
-# df_edge_time = (
-# pd.concat(edge_long, ignore_index=True)
-# if edge_long
-# else pd.DataFrame(columns=["src", "tgt", "type", "weight", "absw", "time"])
-# )
-#
-# fig_anim = _plotly_animated_network(
-# edge_frames=edge_frames,
-# times=times,
-# title=f"{view_mode} network (0–{t_end} min)",
-# node_color_frames=node_color_frames,
-# node_cmax=node_cmax,
-# highlight_nodes=True,
-# )
-# st.plotly_chart(fig_anim, use_container_use_container_width=True, key="anim_net_plot")
-#
-# c_dl1, c_dl2 = st.columns(2)
-# with c_dl1:
-# st.download_button(
-# "Download edge dynamics CSV",
-# data=df_edge_time.to_csv(index=False),
-# file_name=f"edge_dynamics_{view_mode.lower()}_t0-{int(t_end)}_frames{int(n_frames)}.csv",
-# mime="text/csv",
-# key="dl_edge_csv",
-# )
-# with c_dl2:
-# st.download_button(
-# "Download edge dynamics JSON",
-# data=df_edge_time.to_json(orient="records"),
-# file_name=f"edge_dynamics_{view_mode.lower()}_t0-{int(t_end)}_frames{int(n_frames)}.json",
-# mime="application/json",
-# key="dl_edge_json",
-# )
-#
-# # =========================
-# # Matplotlib-based video export (drop-in)
-# # - NO kaleido
-# # - Exports MP4 (ffmpeg) or GIF
-# # - Input: edge_frames (list[pd.DataFrame]), times (array), node_color_frames (optional)
-# # =========================
-# import io
-# import numpy as np
-# import pandas as pd
-# import networkx as nx
-#
-# def _export_network_animation_matplotlib(
-# edge_frames: list[pd.DataFrame],
-# times: np.ndarray,
-# fmt: str = "mp4", # "mp4" or "gif"
-# fps: int = 6,
-# dpi: int = 160,
-# node_cmap: str = "RdBu_r",
-# node_cmax: float = 2.0,
-# node_color_frames: list[np.ndarray] | None = None, # aligned to nodes_union ordering
-# figsize: tuple[float, float] = (9.0, 7.0),
-# ) -> bytes:
-# """
-# Matplotlib animation exporter that avoids plotly/kaleido entirely.
-# Requires:
-# - matplotlib
-# - imageio
-# - for mp4: ffmpeg available via imageio-ffmpeg or system ffmpeg
-# """
-# import matplotlib.pyplot as plt
-# from matplotlib.animation import FuncAnimation, FFMpegWriter, PillowWriter
-# import imageio.v2 as imageio
-#
-# if len(edge_frames) == 0:
-# raise RuntimeError("edge_frames is empty.")
-# if len(times) != len(edge_frames):
-# raise RuntimeError(f"times ({len(times)}) != edge_frames ({len(edge_frames)})")
-#
-# # ---- build stable union graph + layout (fixed for all frames) ----
-# G_union = nx.DiGraph()
-# for df in edge_frames:
-# if df is None or df.empty:
-# continue
-# for r in df.itertuples(index=False):
-# G_union.add_edge(r.src, r.tgt, etype=r.type)
-#
-# if G_union.number_of_nodes() == 0:
-# raise RuntimeError("Union graph has no nodes (all frames empty after filtering).")
-#
-# nodes = list(G_union.nodes())
-# pos = nx.spring_layout(G_union, k=0.8, seed=42)
-#
-# # helpers
-# node_xy = np.array([pos[n] for n in nodes], dtype=float) # (N,2)
-# node2i = {n: i for i, n in enumerate(nodes)}
-#
-# # ---- precompute per-frame edge sets + widths for fast drawing ----
-# # We draw edges as 2 LineCollections: signaling + transcription
-# from matplotlib.collections import LineCollection
-# from matplotlib.colors import Normalize
-#
-# def _edges_to_segments(df: pd.DataFrame, etype: str):
-# if df is None or df.empty:
-# return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
-# sub = df[df["type"] == etype]
-# if sub.empty:
-# return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
-#
-# segs = []
-# widths = []
-# for r in sub.itertuples(index=False):
-# u, v = r.src, r.tgt
-# if u not in node2i or v not in node2i:
-# continue
-# x0, y0 = pos[u]
-# x1, y1 = pos[v]
-# w = float(r.weight)
-# segs.append([[x0, y0], [x1, y1]])
-# # stable width scaling
-# widths.append(float(np.clip(np.log10(1.0 + abs(w)) * 2.5, 0.3, 4.5)))
-# if not segs:
-# return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
-# return np.asarray(segs, dtype=float), np.asarray(widths, dtype=float)
-#
-# sig_segments, sig_widths = [], []
-# tf_segments, tf_widths = [], []
-# for df in edge_frames:
-# s_seg, s_w = _edges_to_segments(df, "signaling")
-# t_seg, t_w = _edges_to_segments(df, "transcription")
-# sig_segments.append(s_seg); sig_widths.append(s_w)
-# tf_segments.append(t_seg); tf_widths.append(t_w)
-#
-# # ---- node colors ----
-# use_node_colors = node_color_frames is not None and len(node_color_frames) == len(edge_frames)
-# norm = Normalize(vmin=-float(node_cmax), vmax=float(node_cmax))
-#
-# # ---- matplotlib figure ----
-# fig, ax = plt.subplots(figsize=figsize)
-# ax.set_axis_off()
-#
-# # two edge layers
-# lc_sig = LineCollection([], linewidths=1.0, alpha=0.55) # color set per-update
-# lc_tf = LineCollection([], linewidths=1.0, alpha=0.55)
-# lc_sig.set_color("#3498db")
-# lc_tf.set_color("#e67e22")
-# ax.add_collection(lc_sig)
-# ax.add_collection(lc_tf)
-#
-# # nodes
-# node_sizes = np.full(len(nodes), 60.0, dtype=float)
-# sc = ax.scatter(node_xy[:, 0], node_xy[:, 1], s=node_sizes, edgecolors="black", linewidths=0.6)
-#
-# # labels (simple; turn off if too slow)
-# texts = []
-# for n in nodes:
-# x, y = pos[n]
-# texts.append(ax.text(x, y, str(n), fontsize=7, ha="center", va="bottom"))
-#
-# title_obj = ax.text(0.01, 0.99, "", transform=ax.transAxes, va="top")
-#
-# # fit bounds
-# pad = 0.08
-# xmin, ymin = node_xy.min(axis=0) - pad
-# xmax, ymax = node_xy.max(axis=0) + pad
-# ax.set_xlim(xmin, xmax)
-# ax.set_ylim(ymin, ymax)
-#
-# # optional colorbar
-# if use_node_colors:
-# import matplotlib.cm as cm
-# mappable = cm.ScalarMappable(norm=norm, cmap=node_cmap)
-# cbar = fig.colorbar(mappable, ax=ax, fraction=0.035, pad=0.02)
-# cbar.set_label("log2(KO/WT)")
-#
-# def _update(i: int):
-# # edges
-# lc_sig.set_segments(sig_segments[i])
-# lc_sig.set_linewidths(sig_widths[i] if len(sig_widths[i]) else 0.2)
-#
-# lc_tf.set_segments(tf_segments[i])
-# lc_tf.set_linewidths(tf_widths[i] if len(tf_widths[i]) else 0.2)
-#
-# # nodes colors
-# if use_node_colors:
-# colors_i = node_color_frames[i]
-# # ensure aligned length; if not, disable
-# if isinstance(colors_i, np.ndarray) and colors_i.shape[0] == len(nodes):
-# sc.set_array(colors_i.astype(float))
-# sc.set_cmap(node_cmap)
-# sc.set_norm(norm)
-#
-# title_obj.set_text(f"Network (t={float(times[i]):.1f} min)")
-# return (lc_sig, lc_tf, sc, title_obj, *texts)
-#
-# anim = FuncAnimation(fig, _update, frames=len(edge_frames), interval=1000 / max(1, fps), blit=False)
-#
-# # ---- write to bytes ----
-# buf = io.BytesIO()
-# fmt_l = fmt.lower()
-#
-# if fmt_l == "mp4":
-# # Preferred: stream directly to BytesIO using FFMpegWriter (works in many environments).
-# try:
-# writer = FFMpegWriter(fps=fps, codec="libx264", bitrate=1800)
-# anim.save(buf, writer=writer, dpi=dpi)
-# plt.close(fig)
-# return buf.getvalue()
-# except Exception:
-# # Fallback: write to temp file then read bytes (more robust).
-# import tempfile, os
-# with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
-# tmp_path = tmp.name
-# try:
-# writer = FFMpegWriter(fps=fps, codec="libx264", bitrate=1800)
-# anim.save(tmp_path, writer=writer, dpi=dpi)
-# with open(tmp_path, "rb") as f:
-# data = f.read()
-# finally:
-# try: os.remove(tmp_path)
-# except Exception: pass
-# plt.close(fig)
-# return data
-#
-# if fmt_l == "gif":
-# try:
-# writer = PillowWriter(fps=fps)
-# anim.save(buf, writer=writer, dpi=dpi)
-# plt.close(fig)
-# return buf.getvalue()
-# except Exception:
-# # fallback: temp file
-# import tempfile, os
-# with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp:
-# tmp_path = tmp.name
-# try:
-# writer = PillowWriter(fps=fps)
-# anim.save(tmp_path, writer=writer, dpi=dpi)
-# with open(tmp_path, "rb") as f:
-# data = f.read()
-# finally:
-# try: os.remove(tmp_path)
-# except Exception: pass
-# plt.close(fig)
-# return data
-#
-# plt.close(fig)
-# raise ValueError("fmt must be 'mp4' or 'gif'")
-#
-#
-# # -------------------------
-# # Streamlit drop-in UI (replace your export block)
-# # -------------------------
-# st.subheader("Export animation (Matplotlib)")
-#
-# cE1, cE2, cE3, cE4 = st.columns(4)
-# with cE1:
-# export_fmt = st.selectbox("Format", ["mp4", "gif"], index=0, key="mpl_export_fmt")
-# with cE2:
-# export_fps = st.slider("FPS", min_value=2, max_value=20, value=6, step=1, key="mpl_export_fps")
-# with cE3:
-# export_dpi = st.slider("DPI", min_value=80, max_value=300, value=160, step=10, key="mpl_export_dpi")
-# with cE4:
-# export_node_cmax = st.slider("Node color range (± log2)", min_value=1.1, max_value=10.0, value=float(node_cmax),
-# step=0.1, key="mpl_export_node_cmax")
-#
-# if st.button("Render export file (Matplotlib)", key="mpl_export_btn"):
-# try:
-# video_bytes = _export_network_animation_matplotlib(
-# edge_frames=edge_frames,
-# times=times,
-# fmt=export_fmt,
-# fps=int(export_fps),
-# dpi=int(export_dpi),
-# node_cmax=float(export_node_cmax),
-# node_color_frames=node_color_frames, # can be None
-# )
-# st.download_button(
-# f"Download {export_fmt.upper()}",
-# data=video_bytes,
-# file_name=f"network_{view_mode.lower()}_t0-{int(t_end)}_frames{int(n_frames)}.{export_fmt}",
-# mime=("video/mp4" if export_fmt == "mp4" else "image/gif"),
-# key="mpl_export_download",
-# )
-# except Exception as e:
-# st.error(str(e))
+# =========================
+# TIME-RESOLVED ANIMATED NETWORK (pre-steady-state)
+# Button-driven + session-state cached.
+# Does NOT recompute on every Streamlit rerun.
+# =========================
+
+st.divider()
+st.header("🕸️ Time-Resolved Animated Network")
+
+# -------------------------
+# State/vector helpers
+# -------------------------
+def _extract_mrna_vec_from_y(y_row: np.ndarray, idx: Index) -> np.ndarray:
+ out = np.zeros(len(idx.proteins), dtype=float)
+ for i in range(len(idx.proteins)):
+ st_y = idx.offset_y[i]
+ out[i] = float(y_row[st_y])
+ return out
+
+
+def _extract_total_protein_vec_from_y(y_row: np.ndarray, idx: Index) -> np.ndarray:
+ out = np.zeros(len(idx.proteins), dtype=float)
+
+ for i in range(len(idx.proteins)):
+ st_y = idx.offset_y[i]
+ prot = float(y_row[st_y + 1])
+ ns = int(idx.n_sites[i])
+
+ if ns > 0:
+ phos_sum = float(np.sum(y_row[st_y + 2: st_y + 2 + ns]))
+ out[i] = prot + phos_sum
+ else:
+ out[i] = prot
+
+ return out
+
+
+def _total_protein_from_Y_row(y_row: np.ndarray, idx: Index, protein: str) -> float:
+ p_i = idx.p2i[protein]
+ st_y = idx.offset_y[p_i]
+
+ prot = float(y_row[st_y + 1])
+ ns = int(idx.n_sites[p_i])
+
+ if ns > 0:
+ return prot + float(np.sum(y_row[st_y + 2: st_y + 2 + ns]))
+
+ return prot
+
+
+def _aligned_log2_ko_wt_for_nodes(
+ nodes: list[str],
+ idx: Index,
+ wt_vec: np.ndarray,
+ ko_vec: np.ndarray,
+ eps: float = 1e-9,
+) -> np.ndarray:
+ """Return log2(KO/WT), aligned to Plotly/NetworkX node order.
+
+ Unknown nodes are set to 0.0 so they appear neutral instead of breaking
+ the color scale.
+ """
+ out = np.zeros(len(nodes), dtype=float)
+
+ for i, node in enumerate(nodes):
+ if node in idx.p2i:
+ j = idx.p2i[node]
+ out[i] = float(np.log2((float(ko_vec[j]) + eps) / (float(wt_vec[j]) + eps)))
+
+ out = np.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0)
+ return out
+
+
+# -------------------------
+# Time-resolved simulation
+# -------------------------
+def _simulate_state_series(params: dict, t_end: float, n_points: int):
+ """Simulate one parameterization once and return a regular time grid."""
+ sys_local, idx_local, _, df_tf_local, _ = load_system(INTENDED_MODEL)
+ sys_local.update(**params)
+
+ t_end = float(t_end)
+ n_points = int(n_points)
+
+ t_target = np.linspace(0.0, t_end, n_points, dtype=float)
+
+ t_sim, Y = simulate_until_steady(
+ sys_local,
+ t_max=t_end,
+ n_points=n_points,
+ )
+
+ t_sim = np.asarray(t_sim, dtype=float)
+ Y = np.asarray(Y, dtype=float)
+
+ if Y.ndim != 2:
+ raise RuntimeError(f"Expected 2D state matrix from simulation, got shape={Y.shape}")
+
+ if len(t_sim) != len(t_target) or not np.allclose(t_sim, t_target):
+ Y_interp = np.empty((len(t_target), Y.shape[1]), dtype=float)
+
+ for j in range(Y.shape[1]):
+ Y_interp[:, j] = np.interp(t_target, t_sim, Y[:, j])
+
+ t_sim = t_target
+ Y = Y_interp
+
+ return sys_local, idx_local, df_tf_local, t_sim, Y
+
+
+# -------------------------
+# Edge construction at one frame
+# -------------------------
+def _edge_tables_from_state_at_time(
+ sys_local: System,
+ idx_local: Index,
+ df_tf_local: pd.DataFrame | None,
+ y_row: np.ndarray,
+ t: float,
+) -> tuple[pd.DataFrame, pd.DataFrame]:
+ """Build signaling and TF edge tables from an already-simulated state row."""
+ t = float(t)
+
+ # Signaling edges: kinase -> target protein.
+ # Kinase activity is represented through external/interpolated kinase input
+ # multiplied by fitted/scaled c_k.
+ Kt = sys_local.kin.eval(t) * sys_local.c_k
+
+ W = sys_local.W_global.tocoo()
+ edge_contrib = W.data * Kt[W.col]
+
+ prot_idx = np.searchsorted(idx_local.offset_s, W.row, side="right") - 1
+ prot_idx = np.clip(prot_idx, 0, len(idx_local.proteins) - 1)
+
+ df_sig = pd.DataFrame(
+ {
+ "src": np.asarray(idx_local.kinases, dtype=object)[W.col],
+ "tgt": np.asarray(idx_local.proteins, dtype=object)[prot_idx],
+ "weight": edge_contrib.astype(float),
+ "type": "signaling",
+ }
+ )
+
+ if not df_sig.empty:
+ df_sig = (
+ df_sig.groupby(["src", "tgt", "type"], as_index=False)
+ .agg(weight=("weight", "sum"))
+ )
+
+ # Transcription edges: TF protein level -> target.
+ df_tf_edges = pd.DataFrame(columns=["src", "tgt", "weight", "type"])
+
+ if df_tf_local is not None and not df_tf_local.empty:
+ tf_mat = sys_local.tf_mat
+ tf_scale = float(getattr(sys_local, "tf_scale", 1.0))
+
+ rows = []
+
+ for r in df_tf_local.itertuples(index=False):
+ tf = getattr(r, "tf")
+ tgt = getattr(r, "target")
+
+ if tf not in idx_local.p2i or tgt not in idx_local.p2i:
+ continue
+
+ i_tgt = idx_local.p2i[tgt]
+ j_tf = idx_local.p2i[tf]
+
+ try:
+ coeff = float(tf_mat[i_tgt, j_tf])
+ except Exception:
+ coeff = float(np.asarray(tf_mat[i_tgt, j_tf]).squeeze())
+
+ if abs(coeff) < 1e-14:
+ continue
+
+ tf_level = _total_protein_from_Y_row(y_row, idx_local, tf)
+ drive = tf_scale * coeff * tf_level
+
+ rows.append((tf, tgt, float(drive), "transcription"))
+
+ if rows:
+ df_tf_edges = pd.DataFrame(
+ rows,
+ columns=["src", "tgt", "weight", "type"],
+ )
+
+ return df_sig, df_tf_edges
+
+
+def _filter_edges_for_animation(
+ df_sig: pd.DataFrame,
+ df_tf: pd.DataFrame,
+ *,
+ include_tf: bool,
+ min_abs_weight: float,
+ top_k: int,
+ edge_scale: float,
+) -> pd.DataFrame:
+ """Merge, clean, scale, threshold, and rank edges for one frame."""
+ if df_sig is None or df_sig.empty:
+ df_all = pd.DataFrame(columns=["src", "tgt", "weight", "type"])
+ else:
+ df_all = df_sig.copy()
+
+ if include_tf and df_tf is not None and not df_tf.empty:
+ df_all = pd.concat([df_all, df_tf], ignore_index=True)
+
+ if df_all.empty:
+ df_all["absw"] = []
+ return df_all
+
+ df_all["src"] = df_all["src"].astype(str)
+ df_all["tgt"] = df_all["tgt"].astype(str)
+ df_all["type"] = df_all["type"].astype(str)
+ df_all["weight"] = pd.to_numeric(df_all["weight"], errors="coerce")
+
+ df_all = (
+ df_all.replace([np.inf, -np.inf], np.nan)
+ .dropna(subset=["src", "tgt", "type", "weight"])
+ .copy()
+ )
+
+ df_all["weight"] = df_all["weight"] * float(edge_scale)
+ df_all["absw"] = df_all["weight"].abs()
+
+ df_all = df_all[df_all["absw"] >= float(min_abs_weight)].copy()
+ df_all = df_all.sort_values("absw", ascending=False).head(int(top_k)).copy()
+
+ return df_all
+
+
+def _delta_edges_for_frame(df_wt_all: pd.DataFrame, df_ko_all: pd.DataFrame) -> pd.DataFrame:
+ """Align WT and KO edges and compute KO - WT edge weight."""
+ key_cols = ["src", "tgt", "type"]
+
+ if df_wt_all.empty and df_ko_all.empty:
+ return pd.DataFrame(columns=["src", "tgt", "type", "weight"])
+
+ if df_wt_all.empty:
+ df_w = pd.DataFrame(columns=key_cols + ["w_wt"])
+ else:
+ df_w = df_wt_all[key_cols + ["weight"]].rename(columns={"weight": "w_wt"})
+
+ if df_ko_all.empty:
+ df_k = pd.DataFrame(columns=key_cols + ["w_ko"])
+ else:
+ df_k = df_ko_all[key_cols + ["weight"]].rename(columns={"weight": "w_ko"})
+
+ df_d = df_k.merge(df_w, on=key_cols, how="outer")
+ df_d["w_ko"] = df_d["w_ko"].fillna(0.0)
+ df_d["w_wt"] = df_d["w_wt"].fillna(0.0)
+ df_d["weight"] = df_d["w_ko"] - df_d["w_wt"]
+
+ return df_d[key_cols + ["weight"]].copy()
+
+
+def _build_animation_frames(
+ *,
+ view_mode: str,
+ best_params: dict,
+ ko_params: dict,
+ include_tf_edges: bool,
+ min_abs_weight: float,
+ top_k: int,
+ edge_scale: float,
+ t_end: float,
+ n_frames: int,
+ node_metric: str,
+):
+ """Generate all animation data once."""
+ sys_wt, idx_wt, df_tf_wt, t_wt, Y_wt = _simulate_state_series(
+ best_params,
+ t_end=float(t_end),
+ n_points=int(n_frames),
+ )
+
+ sys_ko, idx_ko, df_tf_ko, t_ko, Y_ko = _simulate_state_series(
+ ko_params,
+ t_end=float(t_end),
+ n_points=int(n_frames),
+ )
+
+ times = np.asarray(t_wt, dtype=float)
+
+ if len(t_ko) != len(times) or not np.allclose(t_ko, times):
+ raise RuntimeError("WT and KO simulations produced incompatible time grids.")
+
+ edge_frames = []
+
+ for i, tt in enumerate(times):
+ df_sig_wt, df_tf_e_wt = _edge_tables_from_state_at_time(
+ sys_wt,
+ idx_wt,
+ df_tf_wt,
+ Y_wt[i],
+ float(tt),
+ )
+
+ df_sig_ko, df_tf_e_ko = _edge_tables_from_state_at_time(
+ sys_ko,
+ idx_ko,
+ df_tf_ko,
+ Y_ko[i],
+ float(tt),
+ )
+
+ df_wt_all = _filter_edges_for_animation(
+ df_sig_wt,
+ df_tf_e_wt,
+ include_tf=include_tf_edges,
+ min_abs_weight=0.0,
+ top_k=10_000_000,
+ edge_scale=1.0,
+ )
+
+ df_ko_all = _filter_edges_for_animation(
+ df_sig_ko,
+ df_tf_e_ko,
+ include_tf=include_tf_edges,
+ min_abs_weight=0.0,
+ top_k=10_000_000,
+ edge_scale=1.0,
+ )
+
+ if view_mode == "WT":
+ df_e = _filter_edges_for_animation(
+ df_sig_wt,
+ df_tf_e_wt,
+ include_tf=include_tf_edges,
+ min_abs_weight=min_abs_weight,
+ top_k=top_k,
+ edge_scale=edge_scale,
+ )
+
+ elif view_mode == "KO":
+ df_e = _filter_edges_for_animation(
+ df_sig_ko,
+ df_tf_e_ko,
+ include_tf=include_tf_edges,
+ min_abs_weight=min_abs_weight,
+ top_k=top_k,
+ edge_scale=edge_scale,
+ )
+
+ else:
+ df_d = _delta_edges_for_frame(df_wt_all, df_ko_all)
+
+ df_sig_d = df_d[df_d["type"] == "signaling"].copy()
+ df_tf_d = df_d[df_d["type"] == "transcription"].copy()
+
+ df_e = _filter_edges_for_animation(
+ df_sig_d,
+ df_tf_d,
+ include_tf=include_tf_edges,
+ min_abs_weight=min_abs_weight,
+ top_k=top_k,
+ edge_scale=edge_scale,
+ )
+
+ edge_frames.append(df_e)
+
+ # Build union graph for stable node order.
+ G_union = nx.DiGraph()
+
+ for df in edge_frames:
+ if df is None or df.empty:
+ continue
+
+ for r in df.itertuples(index=False):
+ G_union.add_edge(str(r.src), str(r.tgt), etype=str(r.type))
+
+ nodes_union = list(G_union.nodes())
+
+ node_color_frames = None
+
+ if node_metric != "None" and len(nodes_union) > 0:
+ node_color_frames = []
+
+ for i in range(len(times)):
+ if "mRNA" in node_metric:
+ wt_vec = _extract_mrna_vec_from_y(Y_wt[i], idx_wt)
+ ko_vec = _extract_mrna_vec_from_y(Y_ko[i], idx_ko)
+ else:
+ wt_vec = _extract_total_protein_vec_from_y(Y_wt[i], idx_wt)
+ ko_vec = _extract_total_protein_vec_from_y(Y_ko[i], idx_ko)
+
+ node_color_frames.append(
+ _aligned_log2_ko_wt_for_nodes(
+ nodes_union,
+ idx_wt,
+ wt_vec,
+ ko_vec,
+ )
+ )
+
+ edge_long = []
+
+ for tt, df in zip(times, edge_frames):
+ if df is None or df.empty:
+ continue
+
+ tmp = df.copy()
+ tmp["time"] = float(tt)
+ edge_long.append(tmp)
+
+ df_edge_time = (
+ pd.concat(edge_long, ignore_index=True)
+ if edge_long
+ else pd.DataFrame(columns=["src", "tgt", "type", "weight", "absw", "time"])
+ )
+
+ return {
+ "edge_frames": edge_frames,
+ "times": times,
+ "nodes_union": nodes_union,
+ "node_color_frames": node_color_frames,
+ "df_edge_time": df_edge_time,
+ }
+
+
+# -------------------------
+# Plotly animated network
+# -------------------------
+def _build_union_graph_from_edge_frames(edge_frames: list[pd.DataFrame]) -> nx.DiGraph:
+ G = nx.DiGraph()
+
+ for df in edge_frames:
+ if df is None or df.empty:
+ continue
+
+ for r in df.itertuples(index=False):
+ G.add_edge(str(r.src), str(r.tgt), etype=str(r.type))
+
+ return G
+
+
+def _node_activity_frames(edge_frames: list[pd.DataFrame], nodes: list[str]) -> list[np.ndarray]:
+ node2i = {n: i for i, n in enumerate(nodes)}
+ frames = []
+
+ for df in edge_frames:
+ active = np.zeros(len(nodes), dtype=float)
+
+ if df is not None and not df.empty:
+ act_nodes = pd.unique(pd.concat([df["src"], df["tgt"]], ignore_index=True).astype(str))
+
+ for n in act_nodes:
+ j = node2i.get(n)
+ if j is not None:
+ active[j] = 1.0
+
+ frames.append(active)
+
+ return frames
+
+
+def _plotly_animated_network(
+ edge_frames: list[pd.DataFrame],
+ times: np.ndarray,
+ title: str,
+ node_color_frames: list[np.ndarray] | None = None,
+ node_cmax: float = 2.0,
+) -> go.Figure:
+ G = _build_union_graph_from_edge_frames(edge_frames)
+
+ if G.number_of_nodes() == 0:
+ fig = go.Figure()
+ fig.update_layout(
+ title="No edges passed the current filters",
+ template="plotly_white",
+ )
+ return fig
+
+ pos = nx.spring_layout(G, k=0.8, seed=42)
+ nodes = list(G.nodes())
+
+ activity_frames = _node_activity_frames(edge_frames, nodes)
+
+ node_x = [pos[n][0] for n in nodes]
+ node_y = [pos[n][1] for n in nodes]
+
+ base_size = 10.0
+ boost_size = 18.0
+
+ union_edges = list(G.edges())
+ edge_type = {(u, v): G.edges[u, v].get("etype", "signaling") for (u, v) in union_edges}
+
+ def _edge_traces_for_frame(df_edges: pd.DataFrame):
+ traces = []
+
+ if df_edges is None or df_edges.empty:
+ for color in ("#3498db", "#e67e22"):
+ traces.append(
+ go.Scatter(
+ x=[],
+ y=[],
+ mode="lines",
+ line=dict(width=1, color=color),
+ hoverinfo="none",
+ showlegend=False,
+ )
+ )
+ return traces
+
+ wmap = {
+ (str(r.src), str(r.tgt)): float(r.weight)
+ for r in df_edges.itertuples(index=False)
+ }
+
+ for etype, color, label in [
+ ("signaling", "#3498db", "Signaling"),
+ ("transcription", "#e67e22", "Transcription"),
+ ]:
+ x_e, y_e, widths = [], [], []
+
+ for u, v in union_edges:
+ if edge_type[(u, v)] != etype:
+ continue
+
+ w = wmap.get((u, v), 0.0)
+
+ if abs(w) <= 0.0:
+ continue
+
+ x0, y0 = pos[u]
+ x1, y1 = pos[v]
+
+ x_e += [x0, x1, None]
+ y_e += [y0, y1, None]
+ widths.append(float(np.clip(np.log10(1.0 + abs(w)) * 3.0, 0.5, 6.0)))
+
+ # Plotly cannot assign different line widths within one trace.
+ # Use median width per edge type for stable rendering.
+ line_width = float(np.median(widths)) if widths else 0.5
+
+ traces.append(
+ go.Scatter(
+ x=x_e,
+ y=y_e,
+ mode="lines",
+ line=dict(width=line_width, color=color),
+ opacity=0.6,
+ hoverinfo="none",
+ name=label,
+ showlegend=False,
+ )
+ )
+
+ return traces
+
+ def _node_trace_for_frame(i: int):
+ activity = activity_frames[i]
+ sizes = base_size + boost_size * activity
+
+ marker = dict(
+ size=sizes,
+ line=dict(width=1, color="black"),
+ )
+
+ if node_color_frames is not None:
+ colors = np.asarray(node_color_frames[i], dtype=float)
+
+ marker.update(
+ color=colors,
+ colorscale="RdBu_r",
+ cmin=-float(node_cmax),
+ cmax=float(node_cmax),
+ colorbar=dict(title="log2(KO/WT)", thickness=15),
+ )
+
+ return go.Scatter(
+ x=node_x,
+ y=node_y,
+ mode="markers+text",
+ text=nodes,
+ textposition="top center",
+ hovertext=[f"{n}" for n in nodes],
+ hoverinfo="text",
+ marker=marker,
+ showlegend=False,
+ )
+
+ init_data = _edge_traces_for_frame(edge_frames[0]) + [_node_trace_for_frame(0)]
+
+ frames = []
+
+ for i, (df_e, tt) in enumerate(zip(edge_frames, times)):
+ frames.append(
+ go.Frame(
+ data=_edge_traces_for_frame(df_e) + [_node_trace_for_frame(i)],
+ name=str(i),
+ layout=go.Layout(
+ title=f"{title} — t={float(tt):.1f} min"
+ ),
+ )
+ )
+
+ fig = go.Figure(
+ data=init_data,
+ frames=frames,
+ )
+
+ fig.update_layout(
+ title=f"{title} — t={float(times[0]):.1f} min",
+ hovermode="closest",
+ margin=dict(b=0, l=0, r=0, t=60),
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
+ template="plotly_white",
+ updatemenus=[
+ dict(
+ type="buttons",
+ showactive=False,
+ buttons=[
+ dict(
+ label="Play",
+ method="animate",
+ args=[
+ None,
+ dict(
+ frame=dict(duration=250, redraw=True),
+ fromcurrent=True,
+ transition=dict(duration=0),
+ ),
+ ],
+ ),
+ dict(
+ label="Pause",
+ method="animate",
+ args=[
+ [None],
+ dict(
+ frame=dict(duration=0, redraw=False),
+ mode="immediate",
+ transition=dict(duration=0),
+ ),
+ ],
+ ),
+ ],
+ )
+ ],
+ sliders=[
+ dict(
+ active=0,
+ currentvalue=dict(prefix="Time: ", suffix=" min"),
+ steps=[
+ dict(
+ method="animate",
+ args=[
+ [str(i)],
+ dict(
+ mode="immediate",
+ frame=dict(duration=0, redraw=True),
+ transition=dict(duration=0),
+ ),
+ ],
+ label=f"{float(tt):.0f}",
+ )
+ for i, tt in enumerate(times)
+ ],
+ )
+ ],
+ )
+
+ return fig
+
+
+# -------------------------
+# Matplotlib export
+# -------------------------
+def _export_network_animation_matplotlib(
+ edge_frames: list[pd.DataFrame],
+ times: np.ndarray,
+ *,
+ fmt: str = "gif",
+ fps: int = 6,
+ dpi: int = 150,
+ node_cmax: float = 2.0,
+ node_color_frames: list[np.ndarray] | None = None,
+ figsize: tuple[float, float] = (9.0, 7.0),
+) -> bytes:
+ import os
+ import tempfile
+
+ import matplotlib.pyplot as plt
+ from matplotlib.animation import FuncAnimation, FFMpegWriter, PillowWriter
+ from matplotlib.collections import LineCollection
+ from matplotlib.colors import Normalize
+ import matplotlib.cm as cm
+
+ if not edge_frames:
+ raise RuntimeError("No animation frames available. Generate the animated network first.")
+
+ if len(times) != len(edge_frames):
+ raise RuntimeError(f"times length {len(times)} != edge_frames length {len(edge_frames)}")
+
+ G = _build_union_graph_from_edge_frames(edge_frames)
+
+ if G.number_of_nodes() == 0:
+ raise RuntimeError("No nodes available. Lower Min |weight| or increase top edges.")
+
+ nodes = list(G.nodes())
+ pos = nx.spring_layout(G, k=0.8, seed=42)
+
+ node_xy = np.array([pos[n] for n in nodes], dtype=float)
+ node2i = {n: i for i, n in enumerate(nodes)}
+
+ def _segments_for_type(df: pd.DataFrame, etype: str):
+ if df is None or df.empty:
+ return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
+
+ sub = df[df["type"].astype(str) == etype]
+
+ if sub.empty:
+ return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
+
+ segs = []
+ widths = []
+
+ for r in sub.itertuples(index=False):
+ u = str(r.src)
+ v = str(r.tgt)
+
+ if u not in node2i or v not in node2i:
+ continue
+
+ x0, y0 = pos[u]
+ x1, y1 = pos[v]
+ w = float(r.weight)
+
+ segs.append([[x0, y0], [x1, y1]])
+ widths.append(float(np.clip(np.log10(1.0 + abs(w)) * 2.5, 0.3, 4.5)))
+
+ if not segs:
+ return np.zeros((0, 2, 2), dtype=float), np.zeros((0,), dtype=float)
+
+ return np.asarray(segs, dtype=float), np.asarray(widths, dtype=float)
+
+ sig_segments, sig_widths = [], []
+ tf_segments, tf_widths = [], []
+
+ for df in edge_frames:
+ s_seg, s_w = _segments_for_type(df, "signaling")
+ t_seg, t_w = _segments_for_type(df, "transcription")
+
+ sig_segments.append(s_seg)
+ sig_widths.append(s_w)
+ tf_segments.append(t_seg)
+ tf_widths.append(t_w)
+
+ use_node_colors = (
+ node_color_frames is not None
+ and len(node_color_frames) == len(edge_frames)
+ )
+
+ norm = Normalize(vmin=-float(node_cmax), vmax=float(node_cmax))
+
+ fig, ax = plt.subplots(figsize=figsize)
+ ax.set_axis_off()
+
+ lc_sig = LineCollection([], linewidths=1.0, alpha=0.60, color="#3498db")
+ lc_tf = LineCollection([], linewidths=1.0, alpha=0.60, color="#e67e22")
+
+ ax.add_collection(lc_sig)
+ ax.add_collection(lc_tf)
+
+ sc = ax.scatter(
+ node_xy[:, 0],
+ node_xy[:, 1],
+ s=np.full(len(nodes), 65.0),
+ edgecolors="black",
+ linewidths=0.6,
+ )
+
+ labels = [
+ ax.text(
+ pos[n][0],
+ pos[n][1],
+ str(n),
+ fontsize=7,
+ ha="center",
+ va="bottom",
+ )
+ for n in nodes
+ ]
+
+ title_obj = ax.text(
+ 0.01,
+ 0.99,
+ "",
+ transform=ax.transAxes,
+ va="top",
+ fontsize=11,
+ )
+
+ pad = 0.08
+ xmin, ymin = node_xy.min(axis=0) - pad
+ xmax, ymax = node_xy.max(axis=0) + pad
+
+ ax.set_xlim(xmin, xmax)
+ ax.set_ylim(ymin, ymax)
+
+ if use_node_colors:
+ mappable = cm.ScalarMappable(norm=norm, cmap="RdBu_r")
+ cbar = fig.colorbar(mappable, ax=ax, fraction=0.035, pad=0.02)
+ cbar.set_label("log2(KO/WT)")
+
+ def _update(i: int):
+ lc_sig.set_segments(sig_segments[i])
+ lc_sig.set_linewidths(sig_widths[i] if len(sig_widths[i]) else 0.2)
+
+ lc_tf.set_segments(tf_segments[i])
+ lc_tf.set_linewidths(tf_widths[i] if len(tf_widths[i]) else 0.2)
+
+ if use_node_colors:
+ colors_i = np.asarray(node_color_frames[i], dtype=float)
+
+ if colors_i.shape[0] == len(nodes):
+ sc.set_array(colors_i)
+ sc.set_cmap("RdBu_r")
+ sc.set_norm(norm)
+
+ title_obj.set_text(f"Network at t={float(times[i]):.1f} min")
+
+ return (lc_sig, lc_tf, sc, title_obj, *labels)
+
+ anim = FuncAnimation(
+ fig,
+ _update,
+ frames=len(edge_frames),
+ interval=1000 / max(1, int(fps)),
+ blit=False,
+ )
+
+ fmt = fmt.lower().strip()
+
+ if fmt not in {"gif", "mp4"}:
+ plt.close(fig)
+ raise ValueError("fmt must be 'gif' or 'mp4'")
+
+ suffix = f".{fmt}"
+
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
+ tmp_path = tmp.name
+
+ try:
+ if fmt == "gif":
+ writer = PillowWriter(fps=int(fps))
+ else:
+ writer = FFMpegWriter(fps=int(fps), codec="libx264", bitrate=1800)
+
+ anim.save(tmp_path, writer=writer, dpi=int(dpi))
+
+ with open(tmp_path, "rb") as f:
+ data = f.read()
+
+ finally:
+ try:
+ os.remove(tmp_path)
+ except Exception:
+ pass
+
+ plt.close(fig)
+
+ return data
+
+
+# -------------------------
+# Streamlit controls
+# -------------------------
+if target is None:
+ st.info("No KO target is selected. WT view works, but KO/Δ views will be identical or uninformative.")
+
+c1, c2, c3, c4 = st.columns(4)
+
+with c1:
+ anim_view_mode = st.selectbox(
+ "View",
+ ["WT", "KO", "Δ(KO−WT)"],
+ index=1,
+ key="anim_net_view_mode",
+ )
+
+with c2:
+ anim_t_end = st.slider(
+ "End time (min)",
+ min_value=120,
+ max_value=24 * 7 * 60,
+ value=960,
+ step=120,
+ key="anim_net_t_end",
+ )
+
+with c3:
+ anim_n_frames = st.slider(
+ "Frames",
+ min_value=10,
+ max_value=120,
+ value=30,
+ step=5,
+ key="anim_net_n_frames",
+ )
+
+with c4:
+ anim_include_tf = st.checkbox(
+ "Include TF edges",
+ value=True,
+ key="anim_net_include_tf_edges",
+ )
+
+c5, c6, c7, c8 = st.columns(4)
+
+with c5:
+ anim_min_abs_w = st.number_input(
+ "Min |weight| filter",
+ min_value=0.0,
+ value=0.001,
+ step=0.001,
+ format="%.4f",
+ key="anim_net_min_abs_w",
+ )
+
+with c6:
+ anim_top_k = st.slider(
+ "Top edges per frame",
+ min_value=50,
+ max_value=800,
+ value=250,
+ step=50,
+ key="anim_net_top_k",
+ )
+
+with c7:
+ anim_node_metric = st.selectbox(
+ "Node color metric",
+ ["None", "ΔmRNA (log2 KO/WT)", "ΔProtein (log2 KO/WT)"],
+ index=2,
+ key="anim_net_node_metric",
+ )
+
+with c8:
+ anim_node_cmax = st.slider(
+ "Node color range (± log2)",
+ min_value=1.1,
+ max_value=10.0,
+ value=2.0,
+ step=0.1,
+ key="anim_net_node_cmax",
+ )
+
+anim_edge_scale = st.slider(
+ "Edge weight scaling",
+ min_value=0.1,
+ max_value=100.0,
+ value=1.0,
+ step=0.1,
+ key="anim_net_edge_scale",
+)
+
+anim_cache_key = (
+ str(anim_view_mode),
+ float(anim_t_end),
+ int(anim_n_frames),
+ bool(anim_include_tf),
+ float(anim_min_abs_w),
+ int(anim_top_k),
+ str(anim_node_metric),
+ float(anim_node_cmax),
+ float(anim_edge_scale),
+ str(perturbation_signature),
+)
+
+generate_anim = st.button(
+ "Generate animated network",
+ key="anim_net_generate_btn",
+ type="primary",
+)
+
+if generate_anim:
+ with st.spinner("Simulating WT/KO trajectories and building time-resolved network frames..."):
+ anim_payload = _build_animation_frames(
+ view_mode=str(anim_view_mode),
+ best_params=best_params,
+ ko_params=ko_params,
+ include_tf_edges=bool(anim_include_tf),
+ min_abs_weight=float(anim_min_abs_w),
+ top_k=int(anim_top_k),
+ edge_scale=float(anim_edge_scale),
+ t_end=float(anim_t_end),
+ n_frames=int(anim_n_frames),
+ node_metric=str(anim_node_metric),
+ )
+
+ st.session_state["anim_net_cache_key"] = anim_cache_key
+ st.session_state["anim_net_payload"] = anim_payload
+
+has_anim_payload = (
+ st.session_state.get("anim_net_cache_key") == anim_cache_key
+ and "anim_net_payload" in st.session_state
+)
+
+if not has_anim_payload:
+ st.info("Click **Generate animated network** to compute and display this section.")
+else:
+ anim_payload = st.session_state["anim_net_payload"]
+
+ edge_frames = anim_payload["edge_frames"]
+ times = anim_payload["times"]
+ node_color_frames = anim_payload["node_color_frames"]
+ df_edge_time = anim_payload["df_edge_time"]
+
+ fig_anim = _plotly_animated_network(
+ edge_frames=edge_frames,
+ times=times,
+ title=f"{anim_view_mode} network",
+ node_color_frames=node_color_frames,
+ node_cmax=float(anim_node_cmax),
+ )
+
+ st.plotly_chart(fig_anim, use_container_width=True, key="anim_net_plot")
+
+ c_dl1, c_dl2 = st.columns(2)
+
+ with c_dl1:
+ st.download_button(
+ "Download edge dynamics CSV",
+ data=df_edge_time.to_csv(index=False),
+ file_name=(
+ f"edge_dynamics_{str(anim_view_mode).lower()}"
+ f"_t0-{int(anim_t_end)}_frames{int(anim_n_frames)}.csv"
+ ),
+ mime="text/csv",
+ key="anim_net_edge_csv",
+ )
+
+ with c_dl2:
+ st.download_button(
+ "Download edge dynamics JSON",
+ data=df_edge_time.to_json(orient="records"),
+ file_name=(
+ f"edge_dynamics_{str(anim_view_mode).lower()}"
+ f"_t0-{int(anim_t_end)}_frames{int(anim_n_frames)}.json"
+ ),
+ mime="application/json",
+ key="anim_net_edge_json",
+ )
+
+ with st.expander("Preview edge dynamics table"):
+ st.dataframe(df_edge_time.head(500), use_container_width=True)
+
+ st.subheader("Export animation file")
+
+ cE1, cE2, cE3, cE4 = st.columns(4)
+
+ with cE1:
+ export_fmt = st.selectbox(
+ "Format",
+ ["gif", "mp4"],
+ index=0,
+ key="anim_export_fmt",
+ )
+
+ with cE2:
+ export_fps = st.slider(
+ "FPS",
+ min_value=2,
+ max_value=20,
+ value=6,
+ step=1,
+ key="anim_export_fps",
+ )
+
+ with cE3:
+ export_dpi = st.slider(
+ "DPI",
+ min_value=80,
+ max_value=300,
+ value=150,
+ step=10,
+ key="anim_export_dpi",
+ )
+
+ with cE4:
+ export_node_cmax = st.slider(
+ "Export node color range (± log2)",
+ min_value=1.1,
+ max_value=10.0,
+ value=float(anim_node_cmax),
+ step=0.1,
+ key="anim_export_node_cmax",
+ )
+
+ if st.button("Render export file", key="anim_export_btn"):
+ try:
+ with st.spinner("Rendering animation export..."):
+ export_bytes = _export_network_animation_matplotlib(
+ edge_frames=edge_frames,
+ times=times,
+ fmt=str(export_fmt),
+ fps=int(export_fps),
+ dpi=int(export_dpi),
+ node_cmax=float(export_node_cmax),
+ node_color_frames=node_color_frames,
+ )
+
+ st.download_button(
+ f"Download {str(export_fmt).upper()}",
+ data=export_bytes,
+ file_name=(
+ f"network_{str(anim_view_mode).lower()}"
+ f"_t0-{int(anim_t_end)}_frames{int(anim_n_frames)}.{export_fmt}"
+ ),
+ mime=("image/gif" if export_fmt == "gif" else "video/mp4"),
+ key="anim_export_download_btn",
+ )
+
+ except Exception as exc:
+ st.error(f"Animation export failed: {exc}")
\ No newline at end of file
diff --git a/scripts/curve_similarity.py b/scripts/curve_similarity.py
index 31fc476..ed32bea 100644
--- a/scripts/curve_similarity.py
+++ b/scripts/curve_similarity.py
@@ -41,6 +41,7 @@
from __future__ import annotations
+import argparse
from pathlib import Path
import numpy as np
import pandas as pd
@@ -313,5 +314,20 @@ def summarize(name: str, df: pd.DataFrame):
print(" -", out_dir / "frechet_kinopt.csv")
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ """Parse command-line arguments for dashboard and terminal execution."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--tfopt-xlsx", default="data/tfopt_results.xlsx", help="Path to TFOpt result workbook.")
+ parser.add_argument("--kinopt-xlsx", default="data/kinopt_results.xlsx", help="Path to KinOpt result workbook.")
+ parser.add_argument("--out-dir", default="results_scripts", help="Directory where analysis outputs are written.")
+ return parser.parse_args(argv)
+
+
+def cli(argv: list[str] | None = None) -> None:
+ """CLI entry point that forwards parsed arguments to the analysis function."""
+ args = parse_args(argv)
+ main(tfopt_xlsx=args.tfopt_xlsx, kinopt_xlsx=args.kinopt_xlsx, out_dir=args.out_dir)
+
+
if __name__ == "__main__":
- main()
+ cli()
diff --git a/tests/dashboard/test_command_builder.py b/tests/dashboard/test_command_builder.py
new file mode 100644
index 0000000..51602e4
--- /dev/null
+++ b/tests/dashboard/test_command_builder.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from dashboard.command_builder import build_output_dir, build_workflow_command, sanitize_run_name, workflow_arguments
+from dashboard.registry import get_workflow, launchable_workflows, pixi_environments
+
+
+def test_command_construction_for_each_launchable_workflow(tmp_path):
+ keys = {workflow.key for workflow in launchable_workflows()}
+ assert {"prep", "kinopt-local", "tfopt-local", "protwise-model", "networkmodel", "phoskintime-all"} <= keys
+
+ for key in keys:
+ built = build_workflow_command(key, repo_root=tmp_path, pixi_environment="dev", run_name="my run", use_pixi=True)
+ assert built.command[:4] == ["pixi", "run", "-e", "dev"]
+ assert built.command[4:7] == ["python", "-m", built.workflow.python_module]
+ if built.workflow.output_dir_arg:
+ assert built.workflow.output_dir_arg in built.command
+ assert str(built.outdir) in built.command
+ assert built.outdir == tmp_path / "results" / built.workflow.key / "my-run"
+
+
+def test_builds_direct_python_command_without_pixi(tmp_path):
+ built = build_workflow_command("networkmodel", repo_root=tmp_path, run_name="n", use_pixi=False, argument_values={"cores": 2, "scan": True})
+
+ assert built.command[:3] == ["python", "-m", "networkmodel.runner"]
+ assert "pixi" not in built.command
+ assert ["--cores", "2"] == built.command[built.command.index("--cores"):built.command.index("--cores") + 2]
+ assert "--scan" in built.command
+ assert "--output-dir" in built.command
+
+
+def test_phoskintime_all_places_subcommand_before_options(tmp_path):
+ built = build_workflow_command("phoskintime-all", repo_root=tmp_path, run_name="all", use_pixi=False)
+
+ assert built.command[:4] == ["python", "-m", "config.cli", "all"]
+ assert "--outdir" in built.command
+
+
+def test_structured_arguments_reject_unknown_values():
+ workflow = get_workflow("tfopt-local")
+
+ with pytest.raises(ValueError, match="Unsupported arguments"):
+ workflow_arguments(workflow, {"raw_shell": "rm -rf ."})
+
+
+def test_run_name_is_sanitized_and_output_dir_stays_under_base(tmp_path):
+ assert sanitize_run_name("../../bad run!!") == "bad-run"
+
+ outdir = build_output_dir(tmp_path, "kinopt-local", "../../bad run!!")
+
+ assert outdir == tmp_path / "results" / "kinopt-local" / "bad-run"
+
+
+def test_pixi_environment_selection_reads_defined_environments(tmp_path):
+ pixi = tmp_path / "pixi.toml"
+ pixi.write_text('[environments]\ndefault = {}\ndev = {}\nviz = {}\n', encoding="utf-8")
+
+ assert pixi_environments(pixi) == ["default", "dev", "viz"]
+
+
+def test_command_generation_from_assigned_inputs(tmp_path):
+ kinase = tmp_path / "kinase.csv"
+ config = tmp_path / "config.toml"
+ kinase.write_text("a\n", encoding="utf-8")
+ config.write_text("[x]\n", encoding="utf-8")
+
+ built = build_workflow_command(
+ "networkmodel",
+ repo_root=tmp_path,
+ run_name="assigned",
+ use_pixi=False,
+ input_assignments={"kinase_network": kinase, "config": config, "networkmodel_result_dir": tmp_path},
+ )
+
+ assert ["--kinase-net", str(kinase)] == built.command[built.command.index("--kinase-net"):built.command.index("--kinase-net") + 2]
+ assert ["--conf", str(config)] == built.command[built.command.index("--conf"):built.command.index("--conf") + 2]
+ assert "networkmodel_result_dir" not in built.command
+
+
+def test_unsupported_input_roles_are_not_generated(tmp_path):
+ with pytest.raises(ValueError, match="Unsupported input roles"):
+ build_workflow_command("networkmodel", repo_root=tmp_path, input_assignments={"shell": "bad"})
+
+
+def test_networkmodel_command_with_csv_assignments_is_unchanged(tmp_path):
+ kinase = tmp_path / "kinase.csv"
+ tf = tmp_path / "tf.csv"
+ protein = tmp_path / "protein.csv"
+ rna = tmp_path / "rna.csv"
+ phospho = tmp_path / "phospho.csv"
+ for path in (kinase, tf, protein, rna, phospho):
+ path.write_text("col\nvalue\n", encoding="utf-8")
+
+ built = build_workflow_command(
+ "networkmodel",
+ repo_root=tmp_path,
+ run_name="csv-inputs",
+ use_pixi=False,
+ input_assignments={
+ "kinase_network": kinase,
+ "tf_network": tf,
+ "protein_file": protein,
+ "rna_file": rna,
+ "phosphosite_file": phospho,
+ },
+ )
+
+ assert ["--kinase-net", str(kinase)] == built.command[built.command.index("--kinase-net"):built.command.index("--kinase-net") + 2]
+ assert ["--tf-net", str(tf)] == built.command[built.command.index("--tf-net"):built.command.index("--tf-net") + 2]
+ assert ["--ms", str(protein)] == built.command[built.command.index("--ms"):built.command.index("--ms") + 2]
+ assert ["--rna", str(rna)] == built.command[built.command.index("--rna"):built.command.index("--rna") + 2]
+ assert ["--phospho", str(phospho)] == built.command[built.command.index("--phospho"):built.command.index("--phospho") + 2]
diff --git a/tests/dashboard/test_config_utils.py b/tests/dashboard/test_config_utils.py
new file mode 100644
index 0000000..fc0e30e
--- /dev/null
+++ b/tests/dashboard/test_config_utils.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import json
+
+from dashboard.config_utils import DashboardSelection, load_preset, parse_config_file, save_preset
+
+
+def test_parse_json_yaml_and_text_configs(tmp_path):
+ json_path = tmp_path / "config.json"
+ yaml_path = tmp_path / "config.yaml"
+ text_path = tmp_path / "notes.txt"
+ json_path.write_text('{"alpha": 1}', encoding="utf-8")
+ yaml_path.write_text("alpha: 2\nflag: true\n", encoding="utf-8")
+ text_path.write_text("hello", encoding="utf-8")
+
+ assert parse_config_file(json_path) == {"alpha": 1}
+ assert parse_config_file(yaml_path)["alpha"] == 2
+ assert parse_config_file(text_path) == "hello"
+
+
+def test_save_and_load_json_preset(tmp_path):
+ selection = DashboardSelection(
+ workflow_key="networkmodel",
+ run_name="run1",
+ pixi_environment="dev",
+ arguments={"cores": "2"},
+ input_assignments={"kinase_network": "kin.csv"},
+ )
+ path = save_preset(selection, tmp_path / "preset.json")
+
+ loaded = load_preset(path)
+
+ assert json.loads(path.read_text(encoding="utf-8"))["workflow_key"] == "networkmodel"
+ assert loaded.workflow_key == selection.workflow_key
+ assert loaded.arguments == {"cores": "2"}
diff --git a/tests/dashboard/test_file_utils.py b/tests/dashboard/test_file_utils.py
new file mode 100644
index 0000000..ec924e0
--- /dev/null
+++ b/tests/dashboard/test_file_utils.py
@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+from zipfile import ZipFile
+from io import BytesIO
+
+import pytest
+
+from dashboard.file_utils import create_result_zip, filter_by_suffix, human_size, read_text_preview, resolve_directory
+
+
+def test_create_result_zip_archives_files_without_writing_zip(tmp_path):
+ root = tmp_path / "run"
+ (root / "tables").mkdir(parents=True)
+ (root / "tables" / "a.csv").write_text("x\n1\n", encoding="utf-8")
+ (root / "plots").mkdir()
+ (root / "plots" / "fit.png").write_bytes(b"png")
+ (root / "__pycache__").mkdir()
+ (root / "__pycache__" / "ignored.pyc").write_bytes(b"cache")
+
+ archive_bytes = create_result_zip(root)
+
+ with ZipFile(BytesIO(archive_bytes)) as archive:
+ assert sorted(archive.namelist()) == ["plots/fit.png", "tables/a.csv"]
+ assert not list(root.glob("*.zip"))
+
+
+def test_read_text_preview_truncates(tmp_path):
+ path = tmp_path / "console.log"
+ path.write_text("abcdef", encoding="utf-8")
+
+ text, truncated = read_text_preview(path, max_bytes=3)
+
+ assert text == "abc"
+ assert truncated is True
+
+
+def test_filter_by_suffix_and_human_size(tmp_path):
+ csv = tmp_path / "a.csv"
+ png = tmp_path / "b.png"
+ csv.write_text("x", encoding="utf-8")
+ png.write_bytes(b"p")
+
+ assert filter_by_suffix([png, csv], {".csv"}) == [csv]
+ assert human_size(1024) == "1.0 KB"
+
+
+def test_resolve_directory_rejects_missing_path(tmp_path):
+ with pytest.raises(FileNotFoundError):
+ resolve_directory(tmp_path / "missing")
+
+from io import BytesIO
+
+
+from dashboard.file_utils import (
+ create_upload_dir,
+ detect_duplicate_filenames,
+ preview_table,
+ sanitize_filename,
+ save_uploaded_file,
+ validate_existing_file,
+ validate_upload_filename,
+)
+
+
+class DummyUpload:
+ def __init__(self, name: str, data: bytes):
+ self.name = name
+ self._data = data
+
+ def getbuffer(self):
+ return memoryview(self._data)
+
+
+def test_upload_path_creation_and_filename_sanitization(tmp_path):
+ upload_dir = create_upload_dir(tmp_path, "../Bad Run!!")
+
+ assert upload_dir == tmp_path / "dashboard_uploads" / "Bad-Run"
+ assert upload_dir.is_dir()
+ assert sanitize_filename("../bad file.csv") == "bad-file.csv"
+
+
+def test_save_uploaded_file_rejects_empty_and_unsupported(tmp_path):
+ upload_dir = create_upload_dir(tmp_path, "run")
+
+ with pytest.raises(ValueError, match="empty"):
+ save_uploaded_file(DummyUpload("empty.csv", b""), upload_dir)
+ assert validate_upload_filename("bad.exe")
+
+
+def test_duplicate_filename_detection_uses_sanitized_names():
+ assert detect_duplicate_filenames(["a file.csv", "a-file.csv"]) == ["a-file.csv"]
+
+
+def test_preview_csv_tsv_xlsx_helpers(tmp_path):
+ pd = pytest.importorskip("pandas")
+ csv = tmp_path / "a.csv"
+ tsv = tmp_path / "a.tsv"
+ xlsx = tmp_path / "a.xlsx"
+ csv.write_text("a,b\n1,2\n", encoding="utf-8")
+ tsv.write_text("a\tb\n3\t4\n", encoding="utf-8")
+ pd.DataFrame({"a": [5], "b": [6]}).to_excel(xlsx, index=False)
+
+ assert preview_table(csv).iloc[0].to_dict() == {"a": 1, "b": 2}
+ assert preview_table(tsv).iloc[0].to_dict() == {"a": 3, "b": 4}
+ assert preview_table(xlsx).iloc[0].to_dict() == {"a": 5, "b": 6}
+
+
+def test_invalid_existing_file_handling(tmp_path):
+ empty = tmp_path / "empty.csv"
+ empty.write_text("", encoding="utf-8")
+ unsupported = tmp_path / "x.exe"
+ unsupported.write_bytes(b"x")
+
+ assert "File is empty" in validate_existing_file(empty)
+ assert any("Unsupported extension" in problem for problem in validate_existing_file(unsupported))
+
+
+def test_preview_rejects_malformed_excel_with_user_actionable_error(tmp_path):
+ pytest.importorskip("pandas")
+ workbook = tmp_path / "broken.xlsx"
+ workbook.write_bytes(b"not an excel workbook")
+
+ with pytest.raises(Exception):
+ preview_table(workbook)
diff --git a/tests/dashboard/test_git_hygiene.py b/tests/dashboard/test_git_hygiene.py
new file mode 100644
index 0000000..dd38ac3
--- /dev/null
+++ b/tests/dashboard/test_git_hygiene.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+import subprocess
+from pathlib import Path
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+
+
+def test_generated_dashboard_artifacts_are_ignored():
+ gitignore = (REPO_ROOT / ".gitignore").read_text(encoding="utf-8")
+
+ assert "/dashboard_uploads/" in gitignore
+ assert "/results/" in gitignore or "/*results" in gitignore
+ assert "logs/" in gitignore
+ assert "*.zip" in gitignore
+ assert "/.streamlit/" in gitignore or ".streamlit/" in gitignore
+
+
+def test_no_generated_dashboard_folders_are_tracked():
+ tracked = subprocess.check_output(["git", "ls-files"], cwd=REPO_ROOT, text=True).splitlines()
+
+ forbidden_prefixes = ("dashboard_uploads/", "results/", "logs/", ".streamlit/")
+ assert not [path for path in tracked if path.startswith(forbidden_prefixes)]
diff --git a/tests/dashboard/test_importability.py b/tests/dashboard/test_importability.py
new file mode 100644
index 0000000..2890993
--- /dev/null
+++ b/tests/dashboard/test_importability.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+import importlib
+import pkgutil
+import sys
+
+import dashboard
+
+
+def test_dashboard_modules_import_without_starting_streamlit(monkeypatch):
+ """Import dashboard modules without requiring a Streamlit server or optional UI package at import time."""
+ sys.modules.pop("streamlit", None)
+ imported = []
+ for module_info in pkgutil.walk_packages(dashboard.__path__, prefix="dashboard."):
+ module = importlib.import_module(module_info.name)
+ imported.append(module.__name__)
+
+ assert "dashboard.app" in imported
+ assert "dashboard.components.table_viewer" in imported
+ assert "streamlit" not in sys.modules
diff --git a/tests/dashboard/test_registry.py b/tests/dashboard/test_registry.py
new file mode 100644
index 0000000..c4cb592
--- /dev/null
+++ b/tests/dashboard/test_registry.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import json
+
+from dashboard.registry import get_workflow, infer_workflow, launchable_workflows, registered_workflows
+from dashboard.result_parser import discover_result_directory
+
+
+def test_infer_workflow_from_metadata(tmp_path):
+ root = tmp_path / "run"
+ root.mkdir()
+ (root / "metadata.json").write_text(json.dumps({"workflow": "tfopt.local"}), encoding="utf-8")
+
+ descriptor = infer_workflow(discover_result_directory(root))
+
+ assert descriptor.key in {"tfopt.local", "tfopt-local"}
+ assert descriptor.label == "TFOpt local"
+
+
+def test_infer_workflow_from_legacy_networkmodel_tables(tmp_path):
+ root = tmp_path / "legacy"
+ root.mkdir()
+ (root / "scalar_objective.csv").write_text("x\n", encoding="utf-8")
+
+ descriptor = infer_workflow(discover_result_directory(root))
+
+ assert descriptor.key == "networkmodel.runner"
+
+
+def test_registered_workflows_contains_result_and_launcher_entries():
+ keys = {descriptor.key for descriptor in registered_workflows()}
+
+ assert {"kinopt.local", "tfopt.local", "protwise.runner", "networkmodel.runner", "unknown"} <= keys
+ assert {"prep", "kinopt-local", "tfopt-local", "protwise-model", "networkmodel", "phoskintime-all"} <= keys
+
+
+def test_launchable_registry_entries_include_command_metadata():
+ workflows = {workflow.key: workflow for workflow in launchable_workflows()}
+
+ kinopt = workflows["kinopt-local"]
+ assert kinopt.pixi_task == "kinopt-local"
+ assert kinopt.python_module == "kinopt.local"
+ assert kinopt.output_dir_arg == "--outdir"
+ assert kinopt.accepted_arguments
+ assert get_workflow("networkmodel").output_dir_arg == "--output-dir"
+
+
+def _spec(workflow_key: str, role: str):
+ workflow = get_workflow(workflow_key)
+ return next(spec for spec in workflow.input_assignments if spec.role == role)
+
+
+def test_protwise_protein_input_matches_csv_backend_reader():
+ spec = _spec("protwise-model", "protein_file")
+
+ assert spec.extensions == (".csv",)
+ assert "CSV" in spec.label
+ assert "Excel" not in spec.description
+
+
+def test_networkmodel_input_specs_match_csv_backend_readers():
+ roles = ("kinase_network", "tf_network", "protein_file", "rna_file", "phosphosite_file")
+
+ for role in roles:
+ spec = _spec("networkmodel", role)
+ assert spec.extensions == (".csv",)
+ assert "CSV" in spec.label
+
+
+def test_config_input_specs_match_toml_only_runners():
+ for workflow_key, roles in {
+ "kinopt-local": ("config",),
+ "tfopt-local": ("config",),
+ "protwise-model": ("config",),
+ "networkmodel": ("config",),
+ "phoskintime-all": ("tf_config", "kin_config", "model_config"),
+ }.items():
+ for role in roles:
+ spec = _spec(workflow_key, role)
+ assert spec.extensions == (".toml",)
+ assert "TOML" in spec.label
diff --git a/tests/dashboard/test_result_parser.py b/tests/dashboard/test_result_parser.py
new file mode 100644
index 0000000..9830ee4
--- /dev/null
+++ b/tests/dashboard/test_result_parser.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+import json
+
+from dashboard.result_parser import discover_result_directory
+
+
+def test_discovers_standard_result_contract(tmp_path):
+ root = tmp_path / "run1"
+ for dirname in ("tables", "plots", "logs", "reports", "artifacts"):
+ (root / dirname).mkdir(parents=True, exist_ok=True)
+ (root / "metadata.json").write_text(json.dumps({"workflow": "networkmodel.runner"}), encoding="utf-8")
+ (root / "command.txt").write_text("python -m networkmodel.runner", encoding="utf-8")
+ (root / "console.log").write_text("done", encoding="utf-8")
+ (root / "config_resolved.yaml").write_text("x: 1\n", encoding="utf-8")
+ (root / "tables" / "summary.csv").write_text("a\n1\n", encoding="utf-8")
+ (root / "tables" / "summary.tsv").write_text("a\t b\n", encoding="utf-8")
+ (root / "tables" / "workbook.xlsx").write_bytes(b"placeholder")
+ (root / "plots" / "fit.png").write_bytes(b"png")
+ (root / "plots" / "interactive.html").write_text("", encoding="utf-8")
+ (root / "logs" / "worker.log").write_text("worker", encoding="utf-8")
+ (root / "reports" / "report.md").write_text("# Report", encoding="utf-8")
+ (root / "reports" / "report.pdf").write_bytes(b"pdf")
+ (root / "artifacts" / "state.pkl").write_bytes(b"pickle")
+
+ inventory = discover_result_directory(root)
+
+ assert inventory.metadata == root.resolve() / "metadata.json"
+ assert inventory.command == root.resolve() / "command.txt"
+ assert inventory.console_log == root.resolve() / "console.log"
+ assert inventory.config == root.resolve() / "config_resolved.yaml"
+ assert {item.relative_path for item in inventory.tables} == {"tables/summary.csv", "tables/summary.tsv", "tables/workbook.xlsx"}
+ assert {item.relative_path for item in inventory.plots} == {"plots/fit.png", "plots/interactive.html"}
+ assert {item.relative_path for item in inventory.logs} == {"console.log", "logs/worker.log"}
+ assert {item.relative_path for item in inventory.reports} == {"reports/report.md", "reports/report.pdf"}
+ assert {item.relative_path for item in inventory.artifacts} == {"artifacts/state.pkl"}
+ assert inventory.missing_expected == []
+
+
+def test_discovers_legacy_networkmodel_outputs(tmp_path):
+ root = tmp_path / "legacy"
+ root.mkdir()
+ for name in ("scalar_objective.csv", "convergence_history.csv", "pred_prot_picked.csv", "pred_rna_picked.csv", "pred_phospho_picked.csv"):
+ (root / name).write_text("value\n1\n", encoding="utf-8")
+ (root / "optimization").mkdir()
+ (root / "optimization" / "multistart_summary.csv").write_text("x\n", encoding="utf-8")
+ (root / "profiles").mkdir()
+ (root / "profiles" / "profile_likelihood_summary.csv").write_text("x\n", encoding="utf-8")
+ (root / "posterior").mkdir()
+ (root / "posterior" / "posterior_summary.csv").write_text("x\n", encoding="utf-8")
+ (root / "plots").mkdir()
+ (root / "plots" / "ranked_objective.png").write_bytes(b"png")
+
+ inventory = discover_result_directory(root)
+
+ table_names = {item.name for item in inventory.tables}
+ assert "scalar_objective.csv" in table_names
+ assert "convergence_history.csv" in table_names
+ assert "pred_prot_picked.csv" in table_names
+ assert "multistart_summary.csv" in table_names
+ assert "profile_likelihood_summary.csv" in table_names
+ assert "posterior_summary.csv" in table_names
+ assert {item.relative_path for item in inventory.plots} == {"plots/ranked_objective.png"}
+ assert "metadata.json" in inventory.missing_expected
+
+
+def test_discovers_legacy_local_excel_outputs(tmp_path):
+ root = tmp_path / "legacy-local"
+ root.mkdir()
+ (root / "kinopt_results.xlsx").write_bytes(b"xlsx")
+ (root / "tfopt_results.xlsx").write_bytes(b"xlsx")
+
+ inventory = discover_result_directory(root)
+
+ assert {item.name for item in inventory.tables} == {"kinopt_results.xlsx", "tfopt_results.xlsx"}
+
+
+def test_protwise_report_like_html_is_report_not_plot(tmp_path):
+ root = tmp_path / "protwise"
+ root.mkdir()
+ (root / "Protein-wise_report.html").write_text("report", encoding="utf-8")
+ (root / "fit.html").write_text("plot", encoding="utf-8")
+
+ inventory = discover_result_directory(root)
+
+ assert {item.relative_path for item in inventory.reports} == {"Protein-wise_report.html"}
+ assert {item.relative_path for item in inventory.plots} == {"fit.html"}
+
+
+def test_all_workflow_parent_discovers_child_result_directories(tmp_path):
+ root = tmp_path / "all-run"
+ root.mkdir()
+ for dirname in ("tables", "plots", "logs", "reports", "artifacts"):
+ (root / dirname).mkdir()
+ (root / "metadata.json").write_text(json.dumps({"workflow": "phoskintime-all"}), encoding="utf-8")
+ (root / "command.txt").write_text("pixi run phoskintime-all", encoding="utf-8")
+ (root / "console.log").write_text("done", encoding="utf-8")
+ (root / "config_resolved.yaml").write_text("workflow: all\n", encoding="utf-8")
+
+ for child in ("tfopt", "kinopt", "protwise"):
+ child_root = root / child
+ (child_root / "tables").mkdir(parents=True)
+ (child_root / "plots").mkdir()
+ (child_root / "logs").mkdir()
+ (child_root / "reports").mkdir()
+ (child_root / "artifacts").mkdir()
+ (child_root / "metadata.json").write_text(json.dumps({"workflow": child}), encoding="utf-8")
+ (child_root / "tables" / f"{child}_summary.csv").write_text("x\n1\n", encoding="utf-8")
+
+ inventory = discover_result_directory(root)
+
+ assert [path.name for path in inventory.child_runs] == ["kinopt", "protwise", "tfopt"]
+ assert inventory.has_content
+ child_inventory = discover_result_directory(root / "protwise")
+ assert {item.relative_path for item in child_inventory.tables} == {"tables/protwise_summary.csv"}
diff --git a/tests/dashboard/test_runner.py b/tests/dashboard/test_runner.py
new file mode 100644
index 0000000..35c92d2
--- /dev/null
+++ b/tests/dashboard/test_runner.py
@@ -0,0 +1,88 @@
+from __future__ import annotations
+
+import json
+import sys
+
+from dashboard.command_builder import build_workflow_command
+from dashboard.runner import RunEvent, log_tail, run_built_command, stream_command
+
+
+def test_stream_command_uses_shell_false(monkeypatch, tmp_path):
+ calls = {}
+
+ class FakeProcess:
+ stdout = iter(["hello\n"])
+
+ def wait(self):
+ return 0
+
+ def terminate(self):
+ calls["terminated"] = True
+
+ def fake_popen(command, **kwargs):
+ calls["command"] = command
+ calls["shell"] = kwargs.get("shell")
+ return FakeProcess()
+
+ monkeypatch.setattr("dashboard.runner.subprocess.Popen", fake_popen)
+
+ events = list(stream_command(["python", "-c", "print('hello')"], cwd=tmp_path, outdir=tmp_path / "run"))
+
+ assert calls["shell"] is False
+ assert events[-1] == RunEvent(status="success", returncode=0, outdir=(tmp_path / "run").resolve())
+ assert "hello" in (tmp_path / "run" / "console.log").read_text(encoding="utf-8")
+
+
+def test_run_built_command_executes_tiny_python_and_writes_provenance(tmp_path):
+ built = build_workflow_command("prep", repo_root=tmp_path, run_name="tiny", use_pixi=False)
+ command = [sys.executable, "-c", "print('tiny ok')"]
+ built = type(built)(workflow=built.workflow, command=command, outdir=built.outdir, pixi_environment="default")
+
+ events = list(run_built_command(built, repo_root=tmp_path))
+
+ assert events[-1].status == "success"
+ assert events[-1].returncode == 0
+ assert "tiny ok" in (built.outdir / "console.log").read_text(encoding="utf-8")
+ command_text = (built.outdir / "command.txt").read_text(encoding="utf-8").strip()
+ assert str(command[0]) in command_text
+ assert "print" in command_text
+ metadata = json.loads((built.outdir / "metadata.json").read_text(encoding="utf-8"))
+ assert metadata["launcher"] == "dashboard"
+ assert metadata["launcher_status"] == "success"
+ assert metadata["launcher_returncode"] == 0
+
+
+def test_run_built_command_reports_failure_and_log_tail(tmp_path):
+ built = build_workflow_command("prep", repo_root=tmp_path, run_name="fail", use_pixi=False)
+ command = [sys.executable, "-c", "import sys; print('bad'); sys.exit(3)"]
+ built = type(built)(workflow=built.workflow, command=command, outdir=built.outdir, pixi_environment="default")
+
+ events = list(run_built_command(built, repo_root=tmp_path))
+
+ assert events[-1].status == "failure"
+ assert events[-1].returncode == 3
+ assert "bad" in log_tail(built.outdir)
+
+
+def test_stream_command_can_report_cancelled(monkeypatch, tmp_path):
+ calls = {"checks": 0}
+
+ class FakeProcess:
+ stdout = iter(["line\n"])
+
+ def wait(self):
+ return -15
+
+ def terminate(self):
+ calls["terminated"] = True
+
+ monkeypatch.setattr("dashboard.runner.subprocess.Popen", lambda *args, **kwargs: FakeProcess())
+
+ def cancel_after_first_line():
+ calls["checks"] += 1
+ return True
+
+ events = list(stream_command(["python"], cwd=tmp_path, outdir=tmp_path / "run", cancel_check=cancel_after_first_line))
+
+ assert calls["terminated"] is True
+ assert events[-1].status == "cancelled"
diff --git a/tests/dashboard/test_upload_validation.py b/tests/dashboard/test_upload_validation.py
new file mode 100644
index 0000000..19a710f
--- /dev/null
+++ b/tests/dashboard/test_upload_validation.py
@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+from dashboard.components.validation_panel import validate_dashboard_setup
+from dashboard.config_utils import validate_input_assignments
+from dashboard.registry import get_workflow
+
+
+def test_validate_input_assignments_allows_supported_config_toml(tmp_path):
+ config = tmp_path / "config.toml"
+ config.write_text("[x]\ny = 1\n", encoding="utf-8")
+ workflow = get_workflow("networkmodel")
+
+ assert validate_input_assignments(workflow, {"config": str(config)}) == []
+
+
+def test_validate_input_assignments_reports_bad_extension(tmp_path):
+ bad = tmp_path / "kinase.xlsx"
+ bad.write_bytes(b"x")
+ workflow = get_workflow("networkmodel")
+
+ problems = validate_input_assignments(workflow, {"kinase_network": str(bad)})
+
+ assert any("must use one of" in problem for problem in problems)
+
+
+def test_validation_panel_helper_reports_duplicate_and_unknown_role(tmp_path):
+ one = tmp_path / "a file.csv"
+ two = tmp_path / "a-file.csv"
+ one.write_text("a\n1\n", encoding="utf-8")
+ two.write_text("a\n2\n", encoding="utf-8")
+ workflow = get_workflow("tfopt-local")
+
+ problems = validate_dashboard_setup(workflow, [one, two], {"unknown": str(one)})
+
+ assert any("Duplicate filenames" in problem for problem in problems)
+ assert any("Unsupported input role" in problem for problem in problems)
+
+
+def _write_file(path):
+ path.write_text("col\nvalue\n", encoding="utf-8")
+ return path
+
+
+def _validation_problems(workflow_key: str, role: str, path):
+ return validate_input_assignments(get_workflow(workflow_key), {role: str(path)})
+
+
+def test_protwise_protein_input_accepts_csv_and_rejects_xlsx(tmp_path):
+ csv = _write_file(tmp_path / "protein.csv")
+ xlsx = _write_file(tmp_path / "protein.xlsx")
+
+ assert _validation_problems("protwise-model", "protein_file", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("protwise-model", "protein_file", xlsx))
+
+
+def test_networkmodel_kinase_network_accepts_only_csv(tmp_path):
+ csv = _write_file(tmp_path / "kinase.csv")
+ tsv = _write_file(tmp_path / "kinase.tsv")
+ xlsx = _write_file(tmp_path / "kinase.xlsx")
+
+ assert _validation_problems("networkmodel", "kinase_network", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "kinase_network", tsv))
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "kinase_network", xlsx))
+
+
+def test_networkmodel_tf_network_accepts_only_csv(tmp_path):
+ csv = _write_file(tmp_path / "tf.csv")
+ tsv = _write_file(tmp_path / "tf.tsv")
+ xlsx = _write_file(tmp_path / "tf.xlsx")
+
+ assert _validation_problems("networkmodel", "tf_network", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "tf_network", tsv))
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "tf_network", xlsx))
+
+
+def test_networkmodel_ms_protein_data_accepts_only_csv(tmp_path):
+ csv = _write_file(tmp_path / "protein.csv")
+ tsv = _write_file(tmp_path / "protein.tsv")
+ xlsx = _write_file(tmp_path / "protein.xlsx")
+
+ assert _validation_problems("networkmodel", "protein_file", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "protein_file", tsv))
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "protein_file", xlsx))
+
+
+def test_networkmodel_rna_data_accepts_only_csv(tmp_path):
+ csv = _write_file(tmp_path / "rna.csv")
+ tsv = _write_file(tmp_path / "rna.tsv")
+ xlsx = _write_file(tmp_path / "rna.xlsx")
+
+ assert _validation_problems("networkmodel", "rna_file", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "rna_file", tsv))
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "rna_file", xlsx))
+
+
+def test_networkmodel_phospho_data_accepts_only_csv(tmp_path):
+ csv = _write_file(tmp_path / "phospho.csv")
+ tsv = _write_file(tmp_path / "phospho.tsv")
+ xlsx = _write_file(tmp_path / "phospho.xlsx")
+
+ assert _validation_problems("networkmodel", "phosphosite_file", csv) == []
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "phosphosite_file", tsv))
+ assert any(".csv" in problem for problem in _validation_problems("networkmodel", "phosphosite_file", xlsx))
+
+
+def test_workflow_config_inputs_accept_only_toml(tmp_path):
+ toml = _write_file(tmp_path / "config.toml")
+ yaml = _write_file(tmp_path / "config.yaml")
+ yml = _write_file(tmp_path / "config.yml")
+ json_config = _write_file(tmp_path / "config.json")
+
+ for workflow_key, role in (
+ ("kinopt-local", "config"),
+ ("tfopt-local", "config"),
+ ("protwise-model", "config"),
+ ("networkmodel", "config"),
+ ("phoskintime-all", "tf_config"),
+ ("phoskintime-all", "kin_config"),
+ ("phoskintime-all", "model_config"),
+ ):
+ assert _validation_problems(workflow_key, role, toml) == []
+ for invalid in (yaml, yml, json_config):
+ problems = _validation_problems(workflow_key, role, invalid)
+ assert any(".toml" in problem for problem in problems)
diff --git a/tests/dashboard/test_workflow_panels.py b/tests/dashboard/test_workflow_panels.py
new file mode 100644
index 0000000..f6bb73a
--- /dev/null
+++ b/tests/dashboard/test_workflow_panels.py
@@ -0,0 +1,224 @@
+from __future__ import annotations
+
+import ast
+from pathlib import Path
+
+import pytest
+from dashboard.workflow_panels.analysis import build_analysis_command, discover_analysis_outputs
+from dashboard.workflow_panels.kinopt import discover_kinopt_panel
+from dashboard.workflow_panels.networkmodel import discover_networkmodel_panel
+from dashboard.workflow_panels.protwise import discover_protwise_panel
+from dashboard.workflow_panels.tfopt import discover_tfopt_panel
+
+
+def _standard_dirs(root):
+ for name in ("tables", "plots", "logs", "reports", "artifacts"):
+ (root / name).mkdir(parents=True, exist_ok=True)
+
+
+def test_kinopt_and_tfopt_panel_discovery(tmp_path):
+ kin = tmp_path / "kin"
+ tf = tmp_path / "tf"
+ _standard_dirs(kin)
+ _standard_dirs(tf)
+ (kin / "tables" / "kinopt_results.xlsx").write_bytes(b"xlsx")
+ (kin / "plots" / "fit.png").write_bytes(b"png")
+ (tf / "tfopt_results.xlsx").write_bytes(b"xlsx")
+
+ kin_data = discover_kinopt_panel(kin)
+ tf_data = discover_tfopt_panel(tf)
+
+ assert kin_data.primary_result == kin.resolve() / "tables" / "kinopt_results.xlsx"
+ assert len(kin_data.plots) == 1
+ assert tf_data.primary_result == tf.resolve() / "tfopt_results.xlsx"
+ assert not tf_data.messages
+
+
+def test_missing_files_do_not_crash_panel_discovery(tmp_path):
+ root = tmp_path / "empty"
+ _standard_dirs(root)
+
+ kin_data = discover_kinopt_panel(root)
+ tf_data = discover_tfopt_panel(root)
+ prot_data = discover_protwise_panel(root)
+ net_data = discover_networkmodel_panel(root)
+
+ assert kin_data.messages
+ assert tf_data.messages
+ assert prot_data.messages
+ assert net_data.messages
+
+
+def test_protwise_and_networkmodel_discovery(tmp_path):
+ prot = tmp_path / "protwise"
+ net = tmp_path / "network"
+ _standard_dirs(prot)
+ _standard_dirs(net)
+ (prot / "tables" / "fit_residuals.csv").write_text("a\n1\n", encoding="utf-8")
+ (prot / "plots" / "sensitivity.png").write_bytes(b"png")
+ (net / "scalar_objective.csv").write_text("scalar_objective\n1\n", encoding="utf-8")
+ (net / "pred_prot_picked.csv").write_text("protein,pred_fc\nA,1\n", encoding="utf-8")
+ (net / "artifacts" / "dashboard_bundle.pkl").write_bytes(b"bundle")
+ (net / "optimization").mkdir()
+ (net / "optimization" / "best_fit.csv").write_text("x\n1\n", encoding="utf-8")
+
+ prot_data = discover_protwise_panel(prot)
+ net_data = discover_networkmodel_panel(net)
+
+ assert "tables/fit_residuals.csv" in prot_data.tables
+ assert len(prot_data.plots) == 1
+ assert net_data.primary_result == net.resolve() / "artifacts" / "dashboard_bundle.pkl"
+ assert "scalar_objective.csv" in net_data.tables
+ assert "optimization/best_fit.csv" in net_data.tables
+
+
+def test_analysis_command_wrapper_and_output_discovery(tmp_path):
+ root = tmp_path / "run"
+ _standard_dirs(root)
+ command = build_analysis_command(
+ "export-subnetworks",
+ {"input2": "input2.csv", "input4": "input4.csv", "hops": "auto"},
+ root,
+ )
+
+ assert command[:2] == ["python", "scripts/export_subnetworks.py"]
+ assert "--input2" in command
+ assert "--outdir" in command
+ outdir = root / "artifacts" / "analysis" / "export-subnetworks"
+ assert str(outdir) in command
+ (outdir / "index.csv").write_text("x\n", encoding="utf-8")
+
+ outputs = discover_analysis_outputs(root)
+
+ assert outputs["export-subnetworks"] == [outdir / "index.csv"]
+
+
+def _command_options(command):
+ return {part for part in command[2:] if part.startswith("--")}
+
+
+def test_advanced_analysis_commands_match_supported_cli_flags(tmp_path):
+ root = tmp_path / "run"
+ _standard_dirs(root)
+
+ tf_counts = build_analysis_command(
+ "tf-kin-counts",
+ {"tfopt_xlsx": "tf.xlsx", "kinopt_xlsx": "kin.xlsx"},
+ root,
+ )
+ assert _command_options(tf_counts) == {"--tfopt-xlsx", "--kinopt-xlsx", "--out-dir"}
+
+ curve = build_analysis_command(
+ "curve-similarity",
+ {"tfopt_xlsx": "tf.xlsx", "kinopt_xlsx": "kin.xlsx"},
+ root,
+ )
+ assert _command_options(curve) == {"--tfopt-xlsx", "--kinopt-xlsx", "--out-dir"}
+
+ mechanistic = build_analysis_command(
+ "mechanistic-insights",
+ {
+ "kinase_net": "kinase.csv",
+ "tf_net": "tf.csv",
+ "ms": "protein.csv",
+ "rna": "rna.csv",
+ "phospho": "phospho.csv",
+ "kinopt": "kinopt.json",
+ "tfopt": "tfopt.json",
+ "cores": 2,
+ },
+ root,
+ )
+ assert "--output-dir" in mechanistic
+ assert "--results-dir" not in mechanistic
+ assert "--out-dir" not in mechanistic
+ assert str(root) in mechanistic
+
+
+
+
+def _script_argparse_flags(script: str) -> set[str]:
+ tree = ast.parse(Path(script).read_text(encoding="utf-8"))
+ flags: set[str] = set()
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.Call):
+ continue
+ func = node.func
+ if not isinstance(func, ast.Attribute) or func.attr != "add_argument":
+ continue
+ for arg in node.args:
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and arg.value.startswith("--"):
+ flags.add(arg.value)
+ return flags
+
+
+def test_generated_analysis_commands_use_target_script_argparse_flags(tmp_path):
+ root = tmp_path / "run"
+ _standard_dirs(root)
+ command_values = {
+ "tf-kin-counts": {"tfopt_xlsx": "tf.xlsx", "kinopt_xlsx": "kin.xlsx"},
+ "curve-similarity": {"tfopt_xlsx": "tf.xlsx", "kinopt_xlsx": "kin.xlsx"},
+ "export-subnetworks": {"input2": "input2.csv", "input4": "input4.csv", "hops": "auto"},
+ "protein-accumulators": {"prot": "prot.csv", "rna": "rna.csv", "threshold": 2.0},
+ "mechanistic-insights": {
+ "kinase_net": "kinase.csv",
+ "tf_net": "tf.csv",
+ "ms": "protein.csv",
+ "rna": "rna.csv",
+ "phospho": "phospho.csv",
+ "kinopt": "kinopt.json",
+ "tfopt": "tfopt.json",
+ "cores": 2,
+ },
+ "temporal-sensitivity": {"results_dir": str(root), "samples": 8},
+ }
+
+ for task_key, values in command_values.items():
+ command = build_analysis_command(task_key, values, root)
+ accepted = _script_argparse_flags(command[1])
+ assert _command_options(command) <= accepted, task_key
+
+
+def test_analyze_tf_kin_counts_cli_uses_selected_paths(monkeypatch, tmp_path):
+ pytest.importorskip("pandas")
+ import importlib
+
+ module = importlib.import_module("scripts.analyze_tf_kin_counts")
+ calls = {}
+
+ def fake_main(*, tfopt_xlsx, kinopt_xlsx, out_dir):
+ calls["tfopt_xlsx"] = tfopt_xlsx
+ calls["kinopt_xlsx"] = kinopt_xlsx
+ calls["out_dir"] = out_dir
+
+ monkeypatch.setattr(module, "main", fake_main)
+ tf = tmp_path / "tf.xlsx"
+ kin = tmp_path / "kin.xlsx"
+ out = tmp_path / "out"
+
+ module.cli(["--tfopt-xlsx", str(tf), "--kinopt-xlsx", str(kin), "--out-dir", str(out)])
+
+ assert calls == {"tfopt_xlsx": str(tf), "kinopt_xlsx": str(kin), "out_dir": str(out)}
+
+
+def test_curve_similarity_cli_uses_selected_paths(monkeypatch, tmp_path):
+ pytest.importorskip("pandas")
+ pytest.importorskip("numpy")
+ import importlib
+
+ module = importlib.import_module("scripts.curve_similarity")
+ calls = {}
+
+ def fake_main(*, tfopt_xlsx, kinopt_xlsx, out_dir):
+ calls["tfopt_xlsx"] = tfopt_xlsx
+ calls["kinopt_xlsx"] = kinopt_xlsx
+ calls["out_dir"] = out_dir
+
+ monkeypatch.setattr(module, "main", fake_main)
+ tf = tmp_path / "tf.xlsx"
+ kin = tmp_path / "kin.xlsx"
+ out = tmp_path / "out"
+
+ module.cli(["--tfopt-xlsx", str(tf), "--kinopt-xlsx", str(kin), "--out-dir", str(out)])
+
+ assert calls == {"tfopt_xlsx": str(tf), "kinopt_xlsx": str(kin), "out_dir": str(out)}
diff --git a/tests/test_combinatorial_memory_safe.py b/tests/test_combinatorial_memory_safe.py
new file mode 100644
index 0000000..8ae881e
--- /dev/null
+++ b/tests/test_combinatorial_memory_safe.py
@@ -0,0 +1,76 @@
+import numpy as np
+import pandas as pd
+import pytest
+
+from networkmodel.models import iter_random_transitions_for_sites, build_random_transitions
+from networkmodel.simulate import combinatorial_site_signals_streaming
+
+
+def _dense_transitions(n_sites):
+ out = []
+ for m in range(1 << n_sites):
+ for j in range(n_sites):
+ if (m & (1 << j)) == 0:
+ out.append((m, m | (1 << j), j))
+ return out
+
+
+@pytest.mark.parametrize("n_sites", [2, 3, 4])
+def test_streaming_bit_extraction_matches_dense(n_sites):
+ rng = np.random.default_rng(123 + n_sites)
+ states = rng.normal(size=(5, 1 << n_sites))
+ m = np.arange(1 << n_sites, dtype=np.uint32)[:, None]
+ j = np.arange(n_sites, dtype=np.uint32)[None, :]
+ bits = ((m >> j) & 1).astype(np.float64)
+ expected = states @ bits
+ actual = combinatorial_site_signals_streaming(states, n_sites)
+ np.testing.assert_allclose(actual, expected, rtol=0.0, atol=1e-12)
+
+
+@pytest.mark.parametrize("n_sites", [0, 1, 2, 3, 4])
+def test_transition_iterator_matches_dense_order(n_sites):
+ assert list(iter_random_transitions_for_sites(n_sites)) == _dense_transitions(n_sites)
+
+
+def test_build_random_transitions_keeps_small_dense_compatibility():
+ class Idx:
+ N = 1
+ n_sites = np.array([2], dtype=np.int32)
+
+ frm, to, site, off, ntr, dense = build_random_transitions(Idx())
+ expected = _dense_transitions(2)
+ assert off.tolist() == [0]
+ assert ntr.tolist() == [len(expected)]
+ assert dense.tolist() == [True]
+ assert list(zip(frm.tolist(), to.tolist(), site.tolist())) == expected
+
+
+def test_build_random_transitions_avoids_large_dense_storage():
+ class Idx:
+ N = 1
+ n_sites = np.array([5], dtype=np.int32)
+
+ frm, to, site, off, ntr, dense = build_random_transitions(Idx(), dense_threshold_sites=4)
+ assert frm.size == to.size == site.size == 0
+ assert off.tolist() == [0]
+ assert ntr.tolist() == [5 * (1 << 4)]
+ assert dense.tolist() == [False]
+
+
+def test_tiny_combinatorial_smoke_signals():
+ states = np.array([[1.0, 2.0, 3.0, 4.0], [2.0, 3.0, 5.0, 7.0]])
+ signals = combinatorial_site_signals_streaming(states, 2)
+ expected = np.array([[6.0, 7.0], [10.0, 12.0]])
+ np.testing.assert_allclose(signals, expected, rtol=0.0, atol=1e-12)
+
+
+def test_memory_guard_message(monkeypatch):
+ import networkmodel.network as network
+
+ monkeypatch.setattr(network, "MODEL", 2)
+ monkeypatch.setattr(network, "COMBINATORIAL_MAX_STATES_PER_PROTEIN", 4)
+ interactions = pd.DataFrame(
+ {"protein": ["P", "P", "P"], "psite": ["S1", "S2", "S3"], "kinase": ["K", "K", "K"]}
+ )
+ with pytest.raises(MemoryError, match="MODEL=2 scales"):
+ network.Index(interactions)
diff --git a/tests/test_networkmodel_runner_config.py b/tests/test_networkmodel_runner_config.py
new file mode 100644
index 0000000..07c6478
--- /dev/null
+++ b/tests/test_networkmodel_runner_config.py
@@ -0,0 +1,150 @@
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+import pytest
+
+
+def _write_network_config(path: Path, outdir: Path) -> None:
+ path.write_text(
+ f'''
+[networkmodel]
+kinase_net = "custom/kinase_network.csv"
+tf_net = "custom/tf_network.csv"
+ms = "custom/protein.csv"
+rna = "custom/rna.csv"
+phospho = "custom/phospho.csv"
+kinopt = "custom/kinopt.xlsx"
+tfopt = "custom/tfopt.xlsx"
+output_dir = "{outdir.as_posix()}"
+cores = 7
+seed = 123
+n_gen = 44
+lambda_prior = 0.31
+lambda_protein = 0.41
+lambda_rna = 0.51
+lambda_phospho = 0.61
+normalize_fc_steady = true
+use_initial_condition_from_data = false
+model = "sequential"
+n_starts = 3
+profile_likelihood = true
+profile_indices = "1,2"
+profile_grid_size = 5
+posterior_sampling = true
+posterior_num_warmup = 6
+posterior_num_samples = 7
+weighting_method_protein = "variance"
+weighting_method_rna = "uniform"
+sensitivity_metric = "total_signal"
+
+[networkmodel.timepoints]
+protein = [0, 10, 20]
+rna = [4, 14, 24]
+phospho_protein = [0, 5, 15]
+
+[networkmodel.solver]
+absolute_tolerance = 1e-6
+relative_tolerance = 1e-5
+max_timesteps = 12345
+''',
+ encoding="utf-8",
+ )
+
+
+def test_importing_runner_does_not_import_networkmodel_config_eagerly():
+ sys.modules.pop("networkmodel.config", None)
+
+ import networkmodel.runner as runner
+
+ assert runner.parse_config_path(["--conf", "custom.toml"]) == Path("custom.toml")
+ assert "networkmodel.config" not in sys.modules
+
+
+def test_custom_conf_is_used_for_networkmodel_defaults(tmp_path, monkeypatch):
+ pytest.importorskip("numpy")
+ from networkmodel import runner
+
+ conf = tmp_path / "custom_config.toml"
+ outdir = tmp_path / "configured-output"
+ _write_network_config(conf, outdir)
+
+ args, _ = runner.parse_runtime_args(["--conf", str(conf)])
+
+ assert args.config_source == "custom"
+ assert Path(args.resolved_config_path) == conf.resolve()
+ assert args.kinase_net == "custom/kinase_network.csv"
+ assert args.tf_net == "custom/tf_network.csv"
+ assert args.output_dir == outdir.as_posix()
+ assert args.cores == 7
+ assert args.n_gen == 44
+ assert args.lambda_prior == pytest.approx(0.31)
+ assert args.lambda_protein == pytest.approx(0.41)
+ assert args.lambda_rna == pytest.approx(0.51)
+ assert args.lambda_phospho == pytest.approx(0.61)
+ assert args.model_code == 1
+ assert args.time_points_protein.tolist() == [0.0, 10.0, 20.0]
+ assert args.time_points_rna.tolist() == [4.0, 14.0, 24.0]
+ assert args.time_points_phospho.tolist() == [0.0, 5.0, 15.0]
+ assert args.raw_config.ode_abs_tol == pytest.approx(1e-6)
+ assert args.raw_config.ode_rel_tol == pytest.approx(1e-5)
+ assert args.raw_config.ode_max_steps == 12345
+
+
+def test_cli_flags_override_custom_conf(tmp_path):
+ pytest.importorskip("numpy")
+ from networkmodel import runner
+
+ conf = tmp_path / "custom_config.toml"
+ configured_outdir = tmp_path / "configured-output"
+ cli_outdir = tmp_path / "cli-output"
+ _write_network_config(conf, configured_outdir)
+
+ args, _ = runner.parse_runtime_args([
+ "--conf", str(conf),
+ "--solver", "optuna",
+ "--output-dir", str(cli_outdir),
+ "--lambda-rna", "9.5",
+ "--cores", "2",
+ ])
+
+ assert args.output_dir == str(cli_outdir)
+ assert args.solver == "optuna"
+ assert args.lambda_rna == pytest.approx(9.5)
+ assert args.cores == 2
+ assert args.kinase_net == "custom/kinase_network.csv"
+
+
+def test_default_config_is_used_when_conf_is_omitted():
+ pytest.importorskip("numpy")
+ from networkmodel import runner
+
+ args, _ = runner.parse_runtime_args([])
+
+ assert args.config_source == "default"
+ assert Path(args.resolved_config_path) == runner.DEFAULT_CONFIG_PATH.resolve()
+ assert args.conf is None
+
+
+def test_networkmodel_initialization_writes_resolved_config_and_metadata(tmp_path):
+ pytest.importorskip("numpy")
+ pytest.importorskip("numba")
+ from networkmodel import runner
+
+ conf = tmp_path / "custom_config.toml"
+ outdir = tmp_path / "configured-output"
+ _write_network_config(conf, outdir)
+
+ args, _ = runner.parse_runtime_args(["--conf", str(conf)])
+ runner.initialize_run_contract(args)
+
+ metadata = json.loads((outdir / "metadata.json").read_text(encoding="utf-8"))
+ assert (outdir / "config_resolved.yaml").is_file()
+ assert metadata["supplied_config_path"] == str(conf)
+ assert metadata["resolved_config_path"] == str(conf.resolve())
+ assert metadata["config_source"] == "custom"
+ assert metadata["effective_inputs"]["kinase_net"] == "custom/kinase_network.csv"
+ assert metadata["effective_settings"]["lambda_rna"] == pytest.approx(0.51)
+ assert metadata["effective_settings"]["time_points_protein"] == [0.0, 10.0, 20.0]
diff --git a/tests/test_protwise_config_handling.py b/tests/test_protwise_config_handling.py
new file mode 100644
index 0000000..5f4b600
--- /dev/null
+++ b/tests/test_protwise_config_handling.py
@@ -0,0 +1,177 @@
+from __future__ import annotations
+
+import json
+import logging
+from pathlib import Path
+
+import pytest
+
+
+def _write_protwise_config(path: Path, outdir: Path) -> None:
+ path.write_text(
+ f'''
+[paths]
+data_dir = "data"
+results_dir = "{outdir.parent.as_posix()}"
+logs_dir = "{(outdir.parent / 'logs').as_posix()}"
+ode_data_dir = "data"
+
+[ode]
+model = "succmod"
+dev_test = true
+y_metric = "variance"
+alpha_ci = 0.9
+
+[ode.bounds]
+mRNA_prod = 11
+mRNA_deg = 12
+protein_prod = 13
+protein_deg = 14
+phospho_prod = 15
+phospho_deg = 16
+
+[ode.bootstrap]
+n = 17
+
+[ode.time]
+protein = [0, 2, 4, 8]
+rna = [4, 8, 16]
+
+[ode.fit]
+use_regularization = false
+
+[ode.fit.composite_weights]
+rmse = 1.1
+mae = 1.2
+var = 1.3
+mse = 1.4
+l2 = 1.5
+
+[ode.sensitivity]
+enabled = false
+perturbation = 0.25
+
+[ode.sensitivity.morris]
+num_trajectories = 12
+num_levels = 6
+
+[ode.inputs]
+protein_excel = "custom/protein.csv"
+psite_excel = "custom/psite.xlsx"
+rna_excel = "custom/rna.xlsx"
+
+[ode.output]
+out_dir_name = "{outdir.name}"
+out_xlsx_name = "custom_results.xlsx"
+''',
+ encoding="utf-8",
+ )
+
+
+def test_custom_protwise_conf_is_honored(tmp_path):
+ pytest.importorskip("numpy")
+ from config.config import parse_args, extract_config
+
+ conf = tmp_path / "protwise.toml"
+ outdir = tmp_path / "configured-out"
+ _write_protwise_config(conf, outdir)
+
+ args = parse_args(["--conf", str(conf)])
+ config = extract_config(args)
+
+ assert config["config_source"] == "custom"
+ assert Path(config["resolved_config_path"]) == conf.resolve()
+ assert config["input_excel_protein"].endswith("custom/protein.csv")
+ assert config["input_excel_psite"].endswith("custom/psite.xlsx")
+ assert config["input_excel_rna"].endswith("custom/rna.xlsx")
+ assert config["bounds"]["A"] == (0.0, 11.0)
+ assert config["bounds"]["B"] == (0.0, 12.0)
+ assert config["bounds"]["C"] == (0.0, 13.0)
+ assert config["bounds"]["D"] == (0.0, 14.0)
+ assert config["bounds"]["S(i)"] == (0.0, 15.0)
+ assert config["bounds"]["D(i)"] == (0.0, 16.0)
+ assert config["bootstraps"] == 17
+ assert config["time_points"].tolist() == [0.0, 2.0, 4.0, 8.0]
+ assert Path(config["outdir"]) == outdir
+
+
+def test_cli_flags_override_custom_protwise_conf(tmp_path):
+ pytest.importorskip("numpy")
+ from config.config import parse_args, extract_config
+
+ conf = tmp_path / "protwise.toml"
+ configured_out = tmp_path / "configured-out"
+ cli_out = tmp_path / "cli-out"
+ _write_protwise_config(conf, configured_out)
+
+ args = parse_args([
+ "--conf", str(conf),
+ "--A-bound", "0,99",
+ "--bootstraps", "3",
+ "--outdir", str(cli_out),
+ ])
+ config = extract_config(args)
+
+ assert config["bounds"]["A"] == (0.0, 99.0)
+ assert config["bootstraps"] == 3
+ assert Path(config["outdir"]) == cli_out
+ assert config["input_excel_protein"].endswith("custom/protein.csv")
+
+
+def test_default_protwise_config_is_used_without_conf():
+ pytest.importorskip("numpy")
+ from config.config import parse_args, extract_config, default_config_path
+
+ args = parse_args([])
+ config = extract_config(args)
+
+ assert config["config_source"] == "default"
+ assert Path(config["resolved_config_path"]) == default_config_path().resolve()
+ assert config["supplied_config_path"] is None
+
+
+def test_config_cli_model_forwards_custom_conf(monkeypatch, tmp_path):
+ pytest.importorskip("typer")
+ from config import cli
+
+ calls = []
+ monkeypatch.setattr(cli, "_run", lambda cmd: calls.append(cmd))
+ conf = tmp_path / "protwise.toml"
+ conf.write_text("[ode]\n", encoding="utf-8")
+
+ cli.model(conf=conf, outdir=tmp_path / "out")
+
+ assert calls == [["protwise.runner.main", "--conf", str(conf), "--outdir", str(tmp_path / "out")]]
+
+
+def test_importing_protwise_runner_does_not_parse_defaults(monkeypatch):
+ import importlib
+
+ runner = importlib.import_module("protwise.runner.main")
+
+ assert runner._parse_config_path(["--conf", "custom.toml"]) == Path("custom.toml").resolve()
+
+
+def test_protwise_run_contract_records_custom_config(tmp_path):
+ pytest.importorskip("numpy")
+ pytest.importorskip("numba")
+ from config.config import parse_args, extract_config
+ from protwise.runner.main import initialize_run_contract
+
+ conf = tmp_path / "protwise.toml"
+ outdir = tmp_path / "configured-out"
+ _write_protwise_config(conf, outdir)
+ args = parse_args(["--conf", str(conf)])
+ config = extract_config(args)
+
+ initialize_run_contract(config, args, logging.getLogger("protwise-test"))
+
+ metadata = json.loads((outdir / "metadata.json").read_text(encoding="utf-8"))
+ assert (outdir / "config_resolved.yaml").is_file()
+ assert metadata["supplied_config_path"] == str(conf)
+ assert metadata["resolved_config_path"] == str(conf.resolve())
+ assert metadata["config_source"] == "custom"
+ assert metadata["effective_inputs"]["protein"].endswith("custom/protein.csv")
+ assert metadata["effective_bounds"]["A"] == [0.0, 11.0]
+ assert metadata["effective_bootstraps"] == 17
+ assert metadata["effective_time_grid"] == [0.0, 2.0, 4.0, 8.0]
diff --git a/tests/test_result_contract.py b/tests/test_result_contract.py
new file mode 100644
index 0000000..e7277f2
--- /dev/null
+++ b/tests/test_result_contract.py
@@ -0,0 +1,96 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from common.results import (
+ STANDARD_SUBDIRS,
+ ensure_result_dir,
+ populate_standard_subdirs,
+ write_command,
+ write_metadata,
+ write_resolved_config,
+)
+
+
+def test_result_contract_helpers_write_provenance(tmp_path):
+ input_file = tmp_path / "input.csv"
+ input_file.write_text("a,b\n1,2\n", encoding="utf-8")
+ outdir = tmp_path / "run"
+
+ paths = ensure_result_dir(outdir)
+ write_command(outdir, ["python", "-m", "kinopt.local", "--outdir", str(outdir)])
+ write_resolved_config(outdir, {"alpha": 1, "nested": {"beta": True}})
+ write_metadata(outdir, "unit.workflow", args={"outdir": outdir}, inputs=[input_file])
+
+ for name in STANDARD_SUBDIRS:
+ assert paths[name].is_dir()
+ assert (outdir / "command.txt").read_text(encoding="utf-8").startswith("python -m kinopt.local")
+ assert (outdir / "config_resolved.yaml").is_file()
+
+ metadata = json.loads((outdir / "metadata.json").read_text(encoding="utf-8"))
+ assert metadata["workflow"] == "unit.workflow"
+ assert metadata["output_directory"] == str(outdir.resolve())
+ assert metadata["python_version"]
+ assert metadata["inputs"][0]["sha256"]
+
+
+def test_metadata_and_resolved_config_preserve_numpy_arrays_as_lists(tmp_path):
+ np = pytest.importorskip("numpy")
+ outdir = tmp_path / "run"
+ time_grid = np.asarray([0.0, 2.0, 4.0, 8.0], dtype=float)
+
+ write_resolved_config(outdir, {"time_grid": time_grid, "scalar": np.float64(1.5)})
+ write_metadata(outdir, "unit.workflow", extra={"effective_time_grid": time_grid})
+
+ metadata = json.loads((outdir / "metadata.json").read_text(encoding="utf-8"))
+ assert metadata["effective_time_grid"] == [0.0, 2.0, 4.0, 8.0]
+
+ resolved = (outdir / "config_resolved.yaml").read_text(encoding="utf-8")
+ assert "[ 0." not in resolved
+ assert "0.0" in resolved
+ assert "8.0" in resolved
+
+
+def test_populate_standard_subdirs_copies_legacy_outputs(tmp_path):
+ outdir = tmp_path / "run"
+ ensure_result_dir(outdir)
+ (outdir / "summary.csv").write_text("x\n1\n", encoding="utf-8")
+ (outdir / "plot.png").write_bytes(b"png")
+ (outdir / "report.html").write_text("", encoding="utf-8")
+ (outdir / "state.pkl").write_bytes(b"pickle")
+
+ populate_standard_subdirs(outdir)
+
+ assert (outdir / "tables" / "summary.csv").is_file()
+ assert (outdir / "plots" / "plot.png").is_file()
+ assert (outdir / "reports" / "report.html").is_file()
+ assert (outdir / "artifacts" / "state.pkl").is_file()
+ # Backward-compatible top-level names are retained.
+ assert (outdir / "summary.csv").is_file()
+
+
+def test_local_workflows_do_not_copy_outputs_to_data_ode():
+ kinopt_main = Path("kinopt/local/__main__.py").read_text(encoding="utf-8")
+ tfopt_main = Path("tfopt/local/__main__.py").read_text(encoding="utf-8")
+
+ assert "ODE_DATA_DIR" not in kinopt_main
+ assert "ODE_DATA_DIR" not in tfopt_main
+ assert "shutil.copy" not in kinopt_main
+ assert "shutil.copy" not in tfopt_main
+
+
+def test_major_cli_parsers_expose_outdir_flags():
+ parser_files = [
+ Path("kinopt/local/config/constants.py"),
+ Path("tfopt/local/config/constants.py"),
+ Path("config/config.py"),
+ Path("networkmodel/runner.py"),
+ Path("config/cli.py"),
+ ]
+ for path in parser_files:
+ text = path.read_text(encoding="utf-8")
+ assert "--outdir" in text, path
+ assert "--output-dir" in text, path
diff --git a/tfopt/local/__main__.py b/tfopt/local/__main__.py
index 68c2cfd..bda7236 100644
--- a/tfopt/local/__main__.py
+++ b/tfopt/local/__main__.py
@@ -1,7 +1,5 @@
-import shutil
-
from config.helpers import location
-from tfopt.local.config.constants import parse_args, OUT_DIR, OUT_FILE, ODE_DATA_DIR
+from tfopt.local.config.constants import parse_args, OUT_FILE, INPUT1, INPUT3, INPUT4, _CFG
from tfopt.local.config.logconf import setup_logger
from tfopt.local.utils.iodata import organize_output_files, create_report
from tfopt.local.exporter.plotout import plot_estimated_vs_observed, plot_multistart_summary_runtime_overlay
@@ -13,6 +11,10 @@
from tfopt.local.utils.params import get_optimization_parameters, postprocess_results
from tfopt.fitanalysis.helper import Plotter
from common.utils import latexit
+from common.results import (
+ attach_file_console_logger, ensure_result_dir, populate_standard_subdirs,
+ write_command, write_metadata, write_resolved_config,
+)
logger = setup_logger()
@@ -24,7 +26,19 @@ def main():
logger.info("[Local Optimization] mRNA-TF Optimization Problem Started")
# STEP 0: Parse command line arguments.
- lb, ub, loss_type = parse_args()
+ lb, ub, loss_type, out_dir = parse_args()
+ result_dirs = ensure_result_dir(out_dir)
+ out_dir = result_dirs["root"]
+ out_file = out_dir / OUT_FILE.name
+ attach_file_console_logger(logger, out_dir)
+ write_command(out_dir)
+ write_resolved_config(out_dir, _CFG)
+ write_metadata(
+ out_dir,
+ workflow="tfopt.local",
+ args={"lower_bound": lb, "upper_bound": ub, "loss_type": loss_type},
+ inputs=[INPUT1, INPUT3, INPUT4],
+ )
# STEP 1: Load and filter the data.
gene_ids, expr_matrix, expr_time_cols, tf_ids, tf_protein, tf_psite_data, tf_psite_labels, tf_time_cols, reg_map = \
@@ -78,18 +92,18 @@ def main():
f"cv={_get_constraint_violation(result)} start_id={getattr(result, 'start_id', None)}")
# Save multistart results to CSV.
- export_multistart_results(all_results).to_csv(OUT_DIR / "multistart_summary.csv", index=False)
+ export_multistart_results(all_results).to_csv(out_dir / "multistart_summary.csv", index=False)
# Save multistart solutions to NPZ.
save_multistart_solutions_npz(
all_results,
- OUT_DIR / "multistart_params.npz",
+ out_dir / "multistart_params.npz",
)
# Save waterfall plot.
plot_multistart_summary_runtime_overlay(
- OUT_DIR / "multistart_summary.csv",
- out_path=OUT_DIR / "multistart_fun_vs_rank_runtime.png",
+ out_dir / "multistart_summary.csv",
+ out_path=out_dir / "multistart_fun_vs_rank_runtime.png",
figsize=(8, 8),
)
@@ -104,14 +118,14 @@ def main():
predictions = compute_predictions(final_x, regulators, tf_protein_matrix, psite_tensor, n_reg, T_use, n_genes,
beta_start_indices, num_psites)
plot_estimated_vs_observed(predictions, expression_matrix, gene_ids, expr_time_cols, regulators,
- tf_protein_matrix, tf_ids, num_targets=n_genes)
+ tf_protein_matrix, tf_ids, num_targets=n_genes, save_path=out_dir)
# Save results to Excel.
save_results_to_excel(gene_ids, tf_ids, final_alpha, final_beta, psite_labels_arr, expression_matrix,
- predictions, result.fun, reg_map)
+ predictions, result.fun, reg_map, filename=out_file)
# Generate plots.
- plotter = Plotter(OUT_FILE, OUT_DIR)
+ plotter = Plotter(out_file, out_dir)
plotter.plot_alpha_distribution()
plotter.plot_beta_barplots()
plotter.plot_heatmap_abs_residuals()
@@ -124,22 +138,22 @@ def main():
plotter.plot_cdf_beta()
plotter.plot_time_wise_residuals()
- # Copy output file to the ODE_DATA_DIR.
- shutil.copy(OUT_FILE, ODE_DATA_DIR / OUT_FILE.name)
# LateX the results
- latexit.main(OUT_DIR)
+ latexit.main(out_dir)
# Organize output files and create a report.
- organize_output_files(OUT_DIR)
- create_report(OUT_DIR)
+ organize_output_files(out_dir)
+ create_report(out_dir)
- logger.info(f'[Local] Report & Results {location(str(OUT_DIR))}')
+ logger.info(f'[Local] Report & Results {location(str(out_dir))}')
# Click to open the report in a web browser.
- for fpath in [OUT_DIR / 'report.html']:
+ for fpath in [out_dir / 'report.html']:
logger.info(f"{fpath.as_uri()}")
+ populate_standard_subdirs(out_dir)
+
if __name__ == "__main__":
main()
diff --git a/tfopt/local/config/constants.py b/tfopt/local/config/constants.py
index 930da68..31de08a 100644
--- a/tfopt/local/config/constants.py
+++ b/tfopt/local/config/constants.py
@@ -46,6 +46,7 @@ def parse_args():
parser = argparse.ArgumentParser(
description="PhosKinTime - SLSQP mRNA-TF Optimization Problem."
)
+ parser.add_argument("--conf", default=None, help="Compatibility option; configuration is loaded before argument parsing.")
parser.add_argument("--lower_bound", type=float, default=float(_CFG.get("lower_bound", -4.0)))
parser.add_argument("--upper_bound", type=float, default=float(_CFG.get("upper_bound", 4.0)))
parser.add_argument(
@@ -55,6 +56,13 @@ def parse_args():
default=int(_CFG.get("loss_type", 5)),
help="0:MSE 1:MAE 2:softl1 3:cauchy 4:arctan 5:elastic-net 6:tikhonov",
)
+ parser.add_argument(
+ "--outdir", "--output-dir",
+ dest="outdir",
+ type=Path,
+ default=OUT_DIR,
+ help="Directory where all run outputs and provenance files are written.",
+ )
args = parser.parse_args()
- return args.lower_bound, args.upper_bound, args.loss_type
+ return args.lower_bound, args.upper_bound, args.loss_type, args.outdir