diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index fc5cf44ff..7b52beecc 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -89,6 +89,15 @@ jobs: poetry build poetry run twine check dist/* + # The tag is already covered by the merged PR's full test gate. Validate + # only the tag-specific artifact here, before it is published. + - name: Smoke-test the release wheel + if: steps.validate_tag.outputs.should_publish == 'true' + run: | + python -m venv /tmp/corneto-release-wheel-smoke + /tmp/corneto-release-wheel-smoke/bin/pip install dist/*.whl + /tmp/corneto-release-wheel-smoke/bin/python -c 'import corneto; print(corneto.__version__)' + # 8. Create GitHub Release with automated notes - name: Create GitHub Release if: steps.validate_tag.outputs.should_publish == 'true' diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e365d6b50..f9b78f005 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -7,24 +7,8 @@ env: on: push: branches: - - dev - main - tags: - - '*' workflow_dispatch: - inputs: - target: - description: "What to build/deploy (dev=latest, main=stable, or tag)" - type: choice - required: true - options: - - dev - - main - - tag - tag: - description: "Tag to deploy when target=tag (e.g., v1.0.0-beta.3)" - type: string - required: false workflow_run: workflows: ["Publish Python Package"] types: [completed] @@ -43,14 +27,7 @@ jobs: with: fetch-depth: 0 fetch-tags: true - ref: >- - ${{ - github.event_name == 'workflow_dispatch' && - (github.event.inputs.target == 'tag' && format('refs/tags/{0}', github.event.inputs.tag) || - format('refs/heads/{0}', github.event.inputs.target)) - || github.event_name == 'workflow_run' && github.event.workflow_run.head_sha - || github.ref - }} + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }} - name: Set up Python & Graphviz uses: actions/setup-python@v5 @@ -89,22 +66,19 @@ jobs: - name: Determine version_folder id: set_version run: | - case "${GITHUB_REF}" in - refs/heads/dev) - echo "version_folder=latest" >> $GITHUB_OUTPUT - ;; - refs/heads/main) - echo "version_folder=stable" >> $GITHUB_OUTPUT - ;; - refs/tags/*) - v="${GITHUB_REF#refs/tags/}" - echo "version_folder=${v}" >> $GITHUB_OUTPUT - ;; - *) - echo "No docs to deploy for ${GITHUB_REF}" >&2 - exit 0 - ;; - esac + if [ "${{ github.event_name }}" = "workflow_run" ]; then + if [[ "${{ github.event.workflow_run.head_branch }}" == v* ]]; then + echo "version_folder=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT + else + echo "Publish workflow did not run for a release tag" >&2 + exit 1 + fi + elif [ "${GITHUB_REF}" = "refs/heads/main" ]; then + echo "version_folder=stable" >> $GITHUB_OUTPUT + else + echo "No documentation target for ${GITHUB_REF}" >&2 + exit 1 + fi - name: Sync GitHub Releases to Docs env: @@ -119,15 +93,6 @@ jobs: DOCS_BASE_URL: https://corneto.org run: poetry run sphinx-build -W --keep-going -b html docs docs/_build/html - - name: Deploy “latest” docs - if: ${{ steps.set_version.outputs.version_folder == 'latest' }} - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_build/html - destination_dir: latest - keep_files: true - - name: Deploy “stable” docs if: ${{ steps.set_version.outputs.version_folder == 'stable' }} uses: peaceiris/actions-gh-pages@v4 @@ -137,8 +102,8 @@ jobs: destination_dir: stable keep_files: true - - name: Deploy versioned docs for tag - if: ${{ startsWith(github.ref, 'refs/tags/') }} + - name: Deploy versioned docs for release tag + if: ${{ steps.set_version.outputs.version_folder != 'stable' }} uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f213c1b4f..7b3d9819d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -4,8 +4,15 @@ name: Unit tests on: - push: pull_request: + branches: + - main + +# A PR into the public trunk is the release gate. Do not repeat this matrix +# for every temporary branch push, merge to main, or release tag. +concurrency: + group: unit-tests-${{ github.event.pull_request.number }} + cancel-in-progress: true env: POETRY_VERSION: "2.4.1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43712a505..e14e0b8e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ For notebook execution in tutorials, we also support [Pixi](https://pixi.sh/) en Once installed, clone the repository and install it with poetry. This will create a virtual environment ready for development: ```bash -git clone git+https://github.com/saezlab/corneto.git@dev +git clone https://github.com/saezlab/corneto.git cd corneto poetry install --with dev ``` @@ -243,7 +243,7 @@ poetry run python docs/tutorials/run_notebooks.py - **Pixi integration**: Tutorial notebooks can use individual `pixi.toml` files for isolated execution environments with specific dependencies. - **Poetry and PEP 621**: The project uses both Poetry (legacy) and modern PEP 621 project configuration in `pyproject.toml`. -Please see [contributing with tutorials](https://github.com/saezlab/corneto/blob/dev/docs/tutorials/README.md) for more information on how to contribute tutorials. +Please see [contributing with tutorials](https://github.com/saezlab/corneto/blob/main/docs/tutorials/README.md) for more information on how to contribute tutorials. ## Releases diff --git a/README.md b/README.md index bb697839e..587dac6fb 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,14 @@ pip install corneto CORNETO provides several optional dependency groups: +- **`plot`**: Graphviz, NetworkX, and Matplotlib visualization support - **`research`**: Full research stack with Gurobi, PICOS, visualization, and network tools - **`os`**: Open-source solvers (SCIP, HiGHS) with visualization and network tools - **`ml`**: Machine learning dependencies (JAX, Keras, scikit-learn) Install any combination with: ```bash +pip install corneto[plot] # Visualization without the full research stack pip install corneto[research,ml] # Multiple extras ``` diff --git a/RELEASE.md b/RELEASE.md index 41dc719cb..941140b18 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,264 +1,88 @@ # Release Process -CORNETO uses an automated tag-based release process powered by Poetry Dynamic Versioning and GitHub Actions. Git tags serve as the single source of truth for versioning - no manual version bumping in files is required. +CORNETO uses public `main` as its trunk. Every public change reaches it through a +pull request, and an annotated Git tag is the single source of truth for a +release version. Poetry Dynamic Versioning reads that tag; do not edit a version +in project files for a release. -**Note**: This document covers the release process for maintainers. For development setup and contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md). +## Public workflow -## Release Workflow +1. Develop and validate changes in the private workbench as needed. +2. Review the exact publishable snapshot for confidential material. +3. Push that snapshot to a temporary public `publication/` branch and + open one pull request into public `main`. +4. The public-main pull request is the release gate. Its CI validates the merge + result with the Python test matrix, quality checks, documentation build, and + package smoke test. +5. After the PR merges, fast-forward the private mirror's `main` branch to + public `main`. +6. Create the release tag from that public `main` commit. -To create a new release: +There is no public `dev` branch in this workflow. The private `dev` branch may +remain an unpublished integration line, but it is never a public promotion +target. -1. **Integrate `dev` into `main`**. - - Merge via pull request (`dev` -> `main`) or fast-forward merge locally. - ```bash - git checkout dev - git pull --ff-only origin dev - git checkout main - git pull --ff-only origin main - git merge --ff-only dev - git push origin main - ``` +## Creating a release -2. **Create and push the release tag from `main`**: - ```bash - poetry install - git checkout main - git pull --ff-only origin main - poetry run release v1.2.3 - ``` - - Use pre-release tags as needed: `v1.2.3-alpha.0`, `v1.2.3-beta.0`, `v1.2.3-rc.0`. - -3. **Automatic pipeline execution**: - - GitHub Actions detects the new tag - - Builds the package using Poetry - - **Creates GitHub Release with automated release notes** - - Publishes to PyPI via OIDC trusted publishing - - Deploys versioned documentation to GitHub Pages - -4. **Sync `main` back into `dev` after tagging**: - - This keeps `dev` reachable from the latest release tags used by dynamic versioning. - - Without this step, `dev` may continue reporting an older pre-release base (e.g., `1.0.0b3+...`). - ```bash - git checkout dev - git pull --ff-only origin dev - git merge --ff-only origin/main - git push origin dev - ``` - -5. **Version resolution**: - - Poetry Dynamic Versioning automatically extracts the version from the Git tag - - The package version in `pyproject.toml` remains at `0.0.0` (placeholder) - - Built packages use the actual tag version (e.g., `1.2.3`) - -6. **Automated Release Notes**: - - GitHub automatically generates release notes based on merged PRs and commits - - Uses conventional commit patterns to categorize changes - - Includes contributor acknowledgments and change summaries - - Release notes can be manually edited after creation if needed - -## Example Release Process +Only tag a clean local checkout on `main` after the public-main PR has passed +and merged: ```bash -# Integrate dev into main -git checkout dev -git pull --ff-only origin dev git checkout main git pull --ff-only origin main -git merge --ff-only dev -git push origin main - -# Create and push a release tag explicitly -poetry run release v1.0.0-beta.4 - -# Sync main back into dev so dev sees the latest release tag ancestry -git checkout dev -git pull --ff-only origin dev -git merge --ff-only origin/main -git push origin dev -``` - -The release pipeline (`.github/workflows/build-and-publish.yml`) will automatically: -- Build source and wheel distributions -- Create a GitHub Release with automated release notes -- Publish to PyPI using trusted publishing -- Deploy documentation with version switcher - -### Release Helper - -Use the helper command to create and push an explicit tag: - -```bash +poetry install +poetry run release v1.2.3 --dry-run poetry run release v1.2.3 -poetry run release v1.0.0-beta.4 -poetry run release 1.0.0-rc.1 # 'v' prefix is optional ``` -It validates release preconditions (clean tree, on `main`, in sync with `origin/main`, -`origin/dev` merged into `main`, tag not already present), then creates an annotated tag and pushes it to `origin`. +Pre-release tags use the same flow, for example `v1.2.3-alpha.0`, +`v1.2.3-beta.0`, or `v1.2.3-rc.0`. The helper confirms that the tree is clean, +the checkout is on and matches the selected remote's `main`, and the tag does +not already exist. Useful options: ```bash -poetry run release v1.0.0-beta.4 --dry-run # validate only -poetry run release v1.0.0-beta.4 --yes # skip confirmation prompt -poetry run release v1.0.0-beta.4 --remote public # use an explicit release remote +poetry run release v1.0.0-rc.1 --dry-run +poetry run release v1.0.0-rc.1 --yes ``` -The remote defaults to `origin`, which is appropriate for a normal clone of -the public CORNETO repository. In a private development workbench where -`origin` points to a private mirror, configure a separate release-only remote -for the public repository and pass it explicitly: +In the private workbench, the public repository is configured as `public` and +must be named explicitly: ```bash -git remote add public https://github.com/saezlab/corneto.git -poetry run release v1.0.0-beta.8 --remote public --dry-run -poetry run release v1.0.0-beta.8 --remote public +poetry run release v1.0.0-rc.1 --remote public --dry-run +poetry run release v1.0.0-rc.1 --remote public ``` -Before tagging, publish selected private changes through a clean branch and a -pull request into public `dev`, followed by a release pull request from public -`dev` into public `main`. Never release a private development branch directly. +## CI responsibilities -### Customizing Release Notes +The release process intentionally gives each workflow one job: -After the automated release is created, you can: -1. Go to the [GitHub Releases page](https://github.com/saezlab/corneto/releases) -2. Edit the release to add additional context, migration guides, or breaking change notices -3. The automated notes will serve as the foundation, with your manual additions +- Pull requests into `main` run the complete release gate. +- Pushes to `main` deploy the stable documentation only. +- Release tags build the exact tagged distributions, check them, publish to + PyPI, create the GitHub Release, and deploy versioned documentation. -## Version Numbering +This avoids repeating the full test matrix for temporary branch pushes, the +post-merge `main` push, and the release tag. The tagged package remains +protected by artifact-specific validation, while the merged PR is the code +quality gate. -Follow [Semantic Versioning](https://semver.org/): -- `MAJOR.MINOR.PATCH` (e.g., `v1.2.3`) -- Use `v` prefix for tags (e.g., `v1.0.0`, not `1.0.0`) -- Pre-releases: `v1.0.0-alpha.0`, `v1.0.0-beta.0`, `v1.0.0-rc.0` +## Local validation -## Prerequisites for Releases - -Before creating a release, ensure the development environment and code quality standards are met: - -### Development Setup -All maintainers should have the development environment properly configured as described in [CONTRIBUTING.md](CONTRIBUTING.md), including: -- Poetry for dependency management -- Pre-commit hooks installed and active - -### Pre-commit Requirements -**Critical**: Pre-commit hooks must be installed and passing for all commits that will be included in the release. The pre-commit configuration ensures: - -- **Conventional commit messages**: Required for automated GitHub release notes generation (feat:, fix:, docs:, etc.) -- **Code quality**: Linting, formatting, and style checks via Ruff -- **Repository hygiene**: File validation, trailing whitespace removal, etc. - -**Important**: Conventional commits are essential for the automated release notes feature. Each commit should follow the pattern: -``` -(): - -[optional body] - -[optional footer(s)] -``` - -Common types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`, `build`, `perf`, `style`, `revert` - -To set up pre-commit hooks: -```bash -poetry run pre-commit install --hook-type pre-commit --hook-type commit-msg -``` +Before opening the public-main PR, run the checks relevant to the change. A +full release-candidate check normally includes: -### Code Quality Validation -Before releasing, run comprehensive quality checks: ```bash -# Tests poetry run pytest - -# Linting and formatting -poetry run ruff check corneto tests +poetry run ruff check --no-fix corneto tests poetry run ruff format --check corneto tests - -# Package metadata poetry check - -# Advisory type-checking baseline (309 errors in Pyrefly 0.60.0 for RC1) -# This report is tracked but is not an RC blocker. -poetry run pyrefly check corneto - -# Documentation and distributions -poetry run sphinx-build -E -a -W --keep-going -b html docs docs/_build/html +poetry run sphinx-build -W --keep-going -b html docs docs/_build/html poetry build poetry run twine check dist/* ``` -## Development Workflow - -The typical development workflow involves: - -1. **Work in the `dev` branch** for ongoing development -2. **Ensure all commits follow conventional commit format** (enforced by pre-commit hooks) -3. **Run quality checks** before merging to main -4. **Integrate into `main`** when ready for release (via pull request or fast-forward merge) -5. **Create release tag** on the `main` branch to trigger automated publishing -6. **Sync `main` back into `dev`** immediately after tagging - -## Technical Details - -### Poetry Dynamic Versioning Configuration - -The project uses Poetry Dynamic Versioning (configured in `pyproject.toml`): - -```toml -[tool.poetry-dynamic-versioning] -enable = true -vcs = "git" -pattern = "default" -style = "pep440" -tagged-metadata = false -``` - -### GitHub Actions Workflow - -The release workflow (`.github/workflows/build-and-publish.yml`) is triggered by: -- Push events to tags matching `v*` pattern -- Uses OIDC trusted publishing for secure PyPI uploads -- Requires no manual secrets or tokens - -### Branch Strategy - -- **`main`**: Stable releases and release tags -- **`dev`**: Active development branch -- Recommended integration: `dev` -> `main` for releases, then `main` -> `dev` immediately after tagging - -### Documentation Version Switcher - -The documentation uses a centralized version switcher that allows users to navigate between different documentation versions (stable, latest, and specific releases). This system is designed to work across all deployed documentation versions automatically. - -#### Architecture - -The version switcher uses a **single, centrally-updated file hosted on GitHub Pages**: - -1. **Switcher Hosting**: `switcher.json` is deployed to the root of the docs site - - URL: `https://corneto.org/switcher.json` (configured via `DOCS_BASE_URL`) - - This URL is configured in `docs/conf.py` via `_switcher_url_with_ts()` - - A timestamp parameter is added to bypass browser caching - -2. **Automated Updates**: When documentation is deployed to GitHub Pages: - - The docs workflow generates `switcher.json` (`scripts/generate_switcher.py`) - - It is published to the site root alongside the redirect page - - A post-deploy patch step updates existing HTML in `gh-pages` to: - - point `theme_switcher_json_url` to `https://corneto.org/switcher.json` - - correct `theme_switcher_version_match` based on folder (`stable/`, `latest/`, `vX.Y.Z/`) - -3. **Version Mapping**: Documentation deployments use the following version identifiers: - - `dev` branch → deployed as `"latest"` - - `main` branch → deployed as `"stable"` (typically marked as preferred) - - Git tags (e.g., `v1.0.0`) → deployed using the tag name - - The `SPHINX_VERSION_MATCH` environment variable (set in `.github/workflows/deploy-docs.yml`) tells the PyData Sphinx Theme which version is currently being viewed. - -4. **Deployment Behavior (Why `destination_dir` is used without `keep_files`)**: - - Each docs build replaces only its own folder (`latest/`, `stable/`, or `vX.Y.Z/`) on the `gh-pages` branch. - - This ensures clean regeneration for a given version (no mixing of old/new files) while leaving other versions untouched. - - The root redirect is deployed with `keep_files: true` so it doesn’t delete any versioned folders. - -#### Manual Switcher Updates - -If the automatic workflow fails or manual intervention is needed, re-run the docs workflow or regenerate and deploy the root artifacts (redirect + `switcher.json`) using the same workflow steps. +The GitHub release pipeline uses OIDC trusted publishing, creates release notes +automatically, and marks alpha, beta, and RC tags as pre-releases. diff --git a/corneto/_plotting.py b/corneto/_plotting.py index 9e4b08a2e..8bf7a7751 100644 --- a/corneto/_plotting.py +++ b/corneto/_plotting.py @@ -60,6 +60,52 @@ def _as_values(value: Any) -> Optional[np.ndarray]: return np.array(value) +def _scaled_magnitudes( + values: Any, + *, + zero_threshold: float = 1e-6, + scale: Optional[str] = "log", + clip_quantile: Optional[float] = 0.05, +) -> np.ndarray: + """Return finite edge magnitudes normalized to ``[0, 1]``. + + Zeros remain zero, including after quantile clipping. ``clip_quantile`` is + the fraction clipped from the upper tail, matching the historical plotting + option while avoiding lower-tail inflation of small or zero values. + """ + array = np.asarray(values, dtype=float) + if array.ndim != 1: + raise ValueError(f"Plot values must be one-dimensional; got shape {array.shape}.") + if not np.all(np.isfinite(array)): + raise ValueError("Plot values must be finite.") + if zero_threshold < 0: + raise ValueError("zero_threshold must be nonnegative.") + + magnitudes = np.abs(array) + magnitudes[magnitudes < zero_threshold] = 0.0 + if scale == "log": + magnitudes = np.log1p(magnitudes) + elif scale == "std": + std = float(np.std(magnitudes)) + if std > 0: + magnitudes = magnitudes / std + elif scale is not None: + raise ValueError(f"Unknown scale '{scale}'. Use None, 'log', or 'std'.") + + nonzero = magnitudes > 0 + if clip_quantile is not None: + if not 0 <= clip_quantile <= 1: + raise ValueError(f"Clipping value must be between 0 and 1, got {clip_quantile}") + if np.any(nonzero): + upper = float(np.quantile(magnitudes[nonzero], 1 - clip_quantile)) + magnitudes[nonzero] = np.minimum(magnitudes[nonzero], upper) + + maximum = float(np.max(magnitudes)) if magnitudes.size else 0.0 + if maximum <= 0: + return np.zeros_like(magnitudes) + return magnitudes / maximum + + def _processor_sign_magnitude(graph: BaseGraph, data: Dict[str, Any], theme: Theme) -> ProcessorOutput: edge_attrs: Dict[int, Dict[str, str]] = {} vertex_attrs: Dict[Union[int, str], Dict[str, str]] = {} @@ -101,28 +147,21 @@ def _processor_metabolism_flux(graph: BaseGraph, data: Dict[str, Any], theme: Th scale = data.get("scale", "log") clip_quantil = data.get("clip_quantil", 0.05) - flow = np.array(flux_values, dtype=float) - flow[np.abs(flow) < zero_flow_threshold] = 0 - if scale is not None: - if scale == "log": - flow = np.sign(flow) * np.log10(np.abs(flow) + 1.0) - elif scale == "std": - std = float(np.std(flow)) - if std > 0: - flow = flow / std - else: - raise ValueError(f"Unknown scale '{scale}'. Use None, 'log', or 'std'.") - if clip_quantil is not None: - flow = clip_quantiles(flow, float(clip_quantil)) - max_flow = max(float(np.max(np.abs(flow))), 1e-6) + flow = np.asarray(flux_values, dtype=float) + magnitudes = _scaled_magnitudes( + flow, + zero_threshold=zero_flow_threshold, + scale=scale, + clip_quantile=None if clip_quantil is None else float(clip_quantil), + ) min_w = float(theme["min_edge_width"]) max_w = float(theme["max_edge_width"]) - for i, v in enumerate(flow): + for i, (v, magnitude) in enumerate(zip(flow, magnitudes)): if scale is None: - edge_width = max_w if abs(v) > 0 else min_w + edge_width = max_w if magnitude > 0 else min_w else: - edge_width = min_w + (max_w - min_w) * abs(v / max_flow) + edge_width = min_w + (max_w - min_w) * magnitude edge_attrs[i] = {"penwidth": str(edge_width)} if v > 0: edge_attrs[i]["color"] = str(theme["positive_color"]) @@ -236,6 +275,59 @@ def _merge_solution_data( return merged +def _resolve_sample_index( + sample: Optional[Union[int, str]], + *, + feature_data: Any, + num_samples: int, +) -> Optional[int]: + if sample is None: + return None + if isinstance(sample, int): + if sample < 0 or sample >= num_samples: + raise ValueError(f"sample index {sample} out of range for {num_samples} samples.") + return sample + samples = getattr(feature_data, "samples", None) + if samples is None: + raise ValueError("A string sample requires feature_data with named samples.") + names = list(samples) + if sample not in samples: + raise ValueError(f"sample '{sample}' not found in feature_data samples.") + index = names.index(sample) + if index >= num_samples: + raise ValueError(f"sample '{sample}' maps to column {index}, but solution data has only {num_samples} columns.") + return index + + +def _select_solution_sample( + data: Dict[str, Any], + *, + sample: Optional[Union[int, str]], + feature_data: Any, +) -> Dict[str, Any]: + selected = dict(data) + for key in ("vertex_values", "edge_values", "flux_values"): + if key not in selected: + continue + values = _as_values(selected[key]) + if values is None or values.ndim <= 1: + continue + if values.ndim != 2: + raise ValueError(f"Plot data '{key}' must be a vector or matrix; got shape {values.shape}.") + if values.shape[1] == 1 and sample is None: + selected[key] = values[:, 0] + continue + index = _resolve_sample_index( + sample, + feature_data=feature_data, + num_samples=values.shape[1], + ) + if index is None: + raise ValueError(f"Plot data '{key}' contains {values.shape[1]} samples; select one with sample=.") + selected[key] = values[:, index] + return selected + + def _resolve_role_styles( preset: Optional[str], role_styles: Optional[Dict[str, Union[str, Dict[str, str]]]], @@ -348,27 +440,144 @@ def _merge_attrs( def _plot_with_networkx(graph: BaseGraph, **kwargs) -> Any: - import matplotlib.pyplot as plt - import networkx as nx - - from corneto.contrib.networkx import corneto_graph_to_networkx - - skip_unsupported_edges = bool(kwargs.get("skip_unsupported_edges", True)) - nx_graph = corneto_graph_to_networkx(graph, skip_unsupported_edges=skip_unsupported_edges) + try: + import matplotlib.colors as mcolors + import matplotlib.pyplot as plt + import networkx as nx + except ImportError as exc: + raise ImportError( + "The NetworkX renderer requires optional plotting dependencies. " + "Install CORNETO with `pip install 'corneto[plot]'`." + ) from exc + + model_keys = { + "graph_attr", + "node_attr", + "edge_attr", + "custom_edge_attr", + "custom_vertex_attr", + "edge_indexes", + "orphan_edges", + } + model = _build_plot_model(graph, **{key: kwargs[key] for key in model_keys if key in kwargs}) + nx_graph = nx.MultiDiGraph() + node_defaults = dict(model["node_defaults"] or {}) + edge_defaults = dict(model["edge_attr"] or {}) + for name, attrs in model["nodes"]: + merged = dict(node_defaults) + merged.update(attrs) + nx_graph.add_node(name, **merged) + for edge_index, (source, target, attrs) in enumerate(model["edges"]): + merged = dict(edge_defaults) + merged.update(attrs) + nx_graph.add_edge(source, target, key=edge_index, **merged) pos = kwargs.get("pos") - if pos is None: - pos = nx.spring_layout(nx_graph) + if pos is not None: + pos = {str(key): value for key, value in pos.items()} + else: + pos = nx.spring_layout(nx_graph, seed=kwargs.get("seed", 0)) ax = kwargs.get("ax") if ax is None: - fig, ax = plt.subplots(figsize=kwargs.get("figsize")) + fig, ax = plt.subplots(figsize=kwargs.get("figsize"), constrained_layout=True) else: fig = ax.figure - nx.draw_networkx(nx_graph, pos=pos, ax=ax, with_labels=kwargs.get("node_labels") is None) - if kwargs.get("node_labels") is not None: - nx.draw_networkx_labels(nx_graph, pos=pos, labels=kwargs["node_labels"], ax=ax) + graphviz_colors = { + "firebrick4": "#8b1a1a", + "dodgerblue4": "#00688b", + "olivedrab3": "#9acd32", + "orangered": "#ff4500", + } + + def mpl_color(value: Any, default: str) -> str: + color = str(value or default) + color = graphviz_colors.get(color.lower(), color) + return color if mcolors.is_color_like(color) else default + + shape_map = { + "box": "s", + "rect": "s", + "rectangle": "s", + "square": "s", + "diamond": "D", + "triangle": "^", + "point": ".", + "circle": "o", + "ellipse": "o", + } + grouped_nodes: Dict[str, List[str]] = {} + for node, attrs in nx_graph.nodes(data=True): + shape = shape_map.get(str(attrs.get("shape", "circle")).lower(), "o") + grouped_nodes.setdefault(shape, []).append(node) + for shape, nodes in grouped_nodes.items(): + attrs = [nx_graph.nodes[node] for node in nodes] + sizes = [float(attr.get("nodesize", 60 if shape == "." else 900)) for attr in attrs] + nx.draw_networkx_nodes( + nx_graph, + pos, + nodelist=nodes, + node_shape=shape, + node_size=sizes, + node_color=[ + mpl_color(attr.get("fillcolor"), "white") if "filled" in str(attr.get("style", "")) else "white" + for attr in attrs + ], + edgecolors=[mpl_color(attr.get("color"), "black") for attr in attrs], + linewidths=[float(attr.get("penwidth", 1.0)) for attr in attrs], + ax=ax, + ) + + for source, target, key, attrs in nx_graph.edges(keys=True, data=True): + style = str(attrs.get("style", "solid")).split(",")[0] + arrowhead = str(attrs.get("arrowhead", "normal")) + arrowstyle = "-[" if arrowhead == "tee" else ("-" if arrowhead == "none" else "-|>") + nx.draw_networkx_edges( + nx_graph, + pos, + edgelist=[(source, target, key)], + edge_color=mpl_color(attrs.get("color"), "black"), + width=float(attrs.get("penwidth", 1.0)), + style=style if style in {"solid", "dashed", "dotted", "dashdot"} else "solid", + arrows=arrowhead != "none", + arrowstyle=arrowstyle, + connectionstyle="arc3,rad=0.05", + ax=ax, + ) + + requested_labels = kwargs.get("node_labels") + if requested_labels is not None: + requested_labels = {str(node): label for node, label in requested_labels.items()} + labels = {} + for node, attrs in nx_graph.nodes(data=True): + if requested_labels is not None: + labels[node] = requested_labels.get(node, node) + else: + labels[node] = attrs.get("label", node) + font_colors = {mpl_color(attrs.get("fontcolor"), "black") for _, attrs in nx_graph.nodes(data=True)} + nx.draw_networkx_labels( + nx_graph, + pos, + labels=labels, + font_color=next(iter(font_colors)) if len(font_colors) == 1 else "black", + font_size=kwargs.get("font_size", 9), + ax=ax, + ) + + edge_labels = { + (source, target, key): attrs["label"] + for source, target, key, attrs in nx_graph.edges(keys=True, data=True) + if attrs.get("label") not in (None, "") + } + if edge_labels: + nx.draw_networkx_edge_labels( + nx_graph, + pos, + edge_labels=edge_labels, + font_size=kwargs.get("edge_font_size", 8), + ax=ax, + ) ax.set_axis_off() return fig @@ -410,6 +619,7 @@ def plot_graph(graph: BaseGraph, renderer: str = "auto", **kwargs) -> Any: if solution_map is not None: merged_solution_map.update(solution_map) data = _merge_solution_data(data, solution, merged_solution_map) + data = _select_solution_sample(data, sample=sample, feature_data=feature_data) if node_roles is None and feature_data is not None: node_roles = _infer_node_roles_from_feature_data(feature_data, sample=sample, role_key=role_key) @@ -613,27 +823,22 @@ def flow_style( scale: Optional[Literal["log", "std"]] = "log", clip_quantil: Optional[float] = 0.05, ): - flow = np.array(P.expr[flow_name].value) - flow[np.abs(flow) < zero_flow_threshold] = 0 - if scale is not None: - if scale == "log": - flow = np.sign(flow) * np.log10(np.abs(flow) + 1.0) - elif scale == "std": - flow = flow / np.std(flow) - else: - raise ValueError(f"Unknown normalization method: {scale}") - if clip_quantil is not None: - flow = clip_quantiles(flow, clip_quantil) - max_flow = max(np.max(np.abs(flow)), 1e-6) + flow = np.asarray(P.expr[flow_name].value, dtype=float) + magnitudes = _scaled_magnitudes( + flow, + zero_threshold=zero_flow_threshold, + scale=scale, + clip_quantile=clip_quantil, + ) edge_attrs = dict() - for i, v in enumerate(flow): + for i, (v, magnitude) in enumerate(zip(flow, magnitudes)): # Apply threshold edge width - if abs(v) > 0: + if magnitude > 0: edge_width = max_edge_width else: edge_width = min_edge_width if scale is not None: - edge_width = min_edge_width + (max_edge_width - min_edge_width) * abs(v / max_flow) + edge_width = min_edge_width + (max_edge_width - min_edge_width) * magnitude edge_attrs[i] = {"penwidth": str(edge_width)} if flow[i] > 0: edge_attrs[i]["color"] = positive_color @@ -726,6 +931,8 @@ def _build_plot_model( edges: List[Tuple[str, str, Dict[str, str]]] = [] is_hypergraph = False + included_nodes = set() + connected_nodes = {str(vertex) for source, target in graph.E for vertex in (*source, *target)} for i, e in enumerate(graph.edges()): if edge_indexes is not None and i not in edge_indexes: continue @@ -743,6 +950,7 @@ def add_node(v_name: str, default_shape: str) -> None: if v_name in custom_vertex_attr_str: attrs.update(custom_vertex_attr_str[v_name]) nodes.append((v_name, attrs)) + included_nodes.add(v_name) if len(s) == 0: v_name = f"e_{i}_source" @@ -785,6 +993,17 @@ def add_node(v_name: str, default_shape: str) -> None: attrs.update(custom_edge_attr.get(i, {})) edges.append((v_s[0], v_t[0], attrs)) + for vertex in graph.V: + vertex_name = str(vertex) + if vertex_name in included_nodes or vertex_name in connected_nodes: + continue + attrs = {} + if "shape" not in node_defaults: + attrs["shape"] = "circle" + if vertex_name in custom_vertex_attr_str: + attrs.update(custom_vertex_attr_str[vertex_name]) + nodes.append((vertex_name, attrs)) + return { "graph_attr": graph_attr, "node_defaults": node_defaults, diff --git a/corneto/methods/imat.py b/corneto/methods/imat.py index 88595fecd..667fd22c8 100644 --- a/corneto/methods/imat.py +++ b/corneto/methods/imat.py @@ -40,7 +40,9 @@ class MultiSampleIMAT(MultiSampleFBA): Used when both types of regularization are needed. Defaults to 0.0. eps (float, optional): Tolerance for considering a flux as non-zero. Defaults to 1e-2. - scale (bool, optional): Whether to scale the weights. Defaults to False. + scale (bool, optional): If True, normalize the nonzero iMAT score + weights independently for each condition so their absolute values + sum to 100. Defaults to False. gpr_field (str, optional): Name of the attribute field containing GPR rules. Defaults to "GPR". high_expression_threshold (Optional[float], optional): Threshold above @@ -198,8 +200,10 @@ def preprocess(self, graph: BaseGraph, data: Data) -> Tuple[BaseGraph, Data]: # Count features with mapping="edge" for feature in sample.features: - if feature.mapping == "edge" and ( - feature.data.get("role") != "objective" or "imat_score" in feature.data + if ( + feature.mapping == "edge" + and (feature.value is not None or "imat_score" in feature.data) + and (feature.data.get("role") != "objective" or "imat_score" in feature.data) ): edge_features_count += 1 diff --git a/corneto/methods/signal/__init__.py b/corneto/methods/signal/__init__.py deleted file mode 100644 index 21c233edc..000000000 --- a/corneto/methods/signal/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Deprecated compatibility namespace for signaling methods.""" - -import warnings - -warnings.warn( - "corneto.methods.signal is deprecated since CORNETO 1.0.0rc1; use " - "corneto.methods.signaling instead. The compatibility package will be " - "removed in CORNETO 2.0.", - FutureWarning, - stacklevel=2, -) diff --git a/corneto/methods/signal/cellnopt_ilp.py b/corneto/methods/signal/cellnopt_ilp.py deleted file mode 100644 index 63941b6ce..000000000 --- a/corneto/methods/signal/cellnopt_ilp.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated compatibility imports for the CellNOpt ILP implementation.""" - -from corneto.methods.signaling.cellnopt_ilp import ( - cellnoptILP, - cno_style, - expand_graph_for_flows, - plot_data, - plot_fitness, -) - -__all__ = [ - "cellnoptILP", - "cno_style", - "expand_graph_for_flows", - "plot_data", - "plot_fitness", -] diff --git a/corneto/methods/signaling/__init__.py b/corneto/methods/signaling/__init__.py index 1ba1a1b95..e49c705e4 100644 --- a/corneto/methods/signaling/__init__.py +++ b/corneto/methods/signaling/__init__.py @@ -1 +1,14 @@ """Signaling-network inference methods.""" + +from corneto.methods.signaling.cellnopt_dag import BooleanReaction, CellNOptDAG +from corneto.methods.signaling.cellnopt_plotting import ( + plot_cellnopt_fit, + plot_cellnopt_model, +) + +__all__ = [ + "BooleanReaction", + "CellNOptDAG", + "plot_cellnopt_fit", + "plot_cellnopt_model", +] diff --git a/corneto/methods/signaling/cellnopt_dag.py b/corneto/methods/signaling/cellnopt_dag.py new file mode 100644 index 000000000..b7a6e9cdb --- /dev/null +++ b/corneto/methods/signaling/cellnopt_dag.py @@ -0,0 +1,691 @@ +"""Flow-based Boolean network inference in the style of CellNOpt.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from numbers import Real +from typing import Any, Optional + +import numpy as np +from scipy.sparse import csr_matrix + +from corneto._constants import VarType +from corneto.backend._base import Backend, ProblemDef +from corneto.data import Data +from corneto.graph import Attr, BaseGraph, EdgeType, Graph +from corneto.methods._base import FlowMethod +from corneto.methods._input_utils import ( + DEFAULT_CONDITION, + data_from_features, + require_mapping, + validate_condition_keys, +) +from corneto.methods._network_utils import augment_with_boundaries + +__all__ = ["BooleanReaction", "CellNOptDAG"] + +_AND_PATTERN = re.compile(r"^and[0-9]+$", re.IGNORECASE) +_ROLE_INPUT = "input" +_ROLE_OUTPUT = "output" +_ROLE_INPUT_OUTPUT = "input_output" + + +@dataclass(frozen=True) +class BooleanReaction: + """A normalized Boolean reaction with one product. + + Positive literals must be active and negative literals must be inactive + for the reaction to propagate in a condition. ``source_edges`` records the + PKN edges from which the reaction was compiled. + """ + + positive_literals: tuple[Any, ...] + negative_literals: tuple[Any, ...] + product: Any + source_edges: tuple[int, ...] + + @property + def literals(self) -> tuple[Any, ...]: + """Return positive and negative literals in deterministic order.""" + return self.positive_literals + self.negative_literals + + +@dataclass(frozen=True) +class _ReactionNetwork: + graph: Graph + reactions: tuple[BooleanReaction, ...] + dependency_reactions: np.ndarray + positive_literals: csr_matrix + negative_literals: csr_matrix + products: csr_matrix + + +def _is_and_vertex(vertex: Any) -> bool: + return isinstance(vertex, str) and _AND_PATTERN.fullmatch(vertex) is not None + + +def _interaction(graph: BaseGraph, edge_index: int) -> int: + value = graph.get_attr_edge(edge_index).get("interaction") + if isinstance(value, bool) or not isinstance(value, Real) or value not in (-1, 1): + raise ValueError(f"CellNOptDAG requires interaction +1 or -1 on edge {edge_index}; got {value!r}.") + return int(value) + + +def _compile_reactions(graph: BaseGraph) -> _ReactionNetwork: + """Compile SIF-style dummy AND vertices into reaction-level dependencies.""" + if graph.num_vertices == 0 or graph.num_edges == 0: + raise ValueError("CellNOptDAG requires a non-empty directed PKN.") + + and_vertices = {vertex for vertex in graph.V if _is_and_vertex(vertex)} + vertex_order = {vertex: index for index, vertex in enumerate(graph.V)} + incoming_by_and = {vertex: [] for vertex in and_vertices} + outgoing_by_and = {vertex: [] for vertex in and_vertices} + + for edge_index, ((source, target), attributes) in enumerate(zip(graph.E, graph.get_attr_edges())): + if len(source) != 1 or len(target) != 1: + raise ValueError( + "CellNOptDAG accepts simple directed PKN edges only; " + f"edge {edge_index} has {len(source)} sources and {len(target)} targets." + ) + if not attributes.has_attr(Attr.EDGE_TYPE, EdgeType.DIRECTED): + raise ValueError(f"CellNOptDAG requires directed edges; edge {edge_index} is not directed.") + _interaction(graph, edge_index) + source_vertex = next(iter(source)) + target_vertex = next(iter(target)) + if source_vertex in and_vertices and target_vertex in and_vertices: + raise ValueError(f"Nested dummy AND gates are not supported (edge {edge_index}).") + if target_vertex in and_vertices: + incoming_by_and[target_vertex].append(edge_index) + if source_vertex in and_vertices: + outgoing_by_and[source_vertex].append(edge_index) + + reactions: list[BooleanReaction] = [] + reaction_keys: set[tuple[frozenset, frozenset, Any]] = set() + + def ordered(vertices): + return tuple(sorted(vertices, key=vertex_order.__getitem__)) + + def add_reaction(positive, negative, product, source_edges): + positive = set(positive) + negative = set(negative) + contradictory = positive & negative + if contradictory: + literal = min(contradictory, key=vertex_order.__getitem__) + raise ValueError(f"Reaction producing {product!r} contains both {literal!r} and !{literal!r}.") + key = (frozenset(positive), frozenset(negative), product) + if key in reaction_keys: + return + reaction_keys.add(key) + reactions.append( + BooleanReaction( + positive_literals=ordered(positive), + negative_literals=ordered(negative), + product=product, + source_edges=tuple(source_edges), + ) + ) + + for edge_index, (source, target) in enumerate(graph.E): + source_vertex = next(iter(source)) + target_vertex = next(iter(target)) + if source_vertex in and_vertices or target_vertex in and_vertices: + continue + sign = _interaction(graph, edge_index) + add_reaction( + [source_vertex] if sign > 0 else [], + [source_vertex] if sign < 0 else [], + target_vertex, + [edge_index], + ) + + for gate in (vertex for vertex in graph.V if vertex in and_vertices): + incoming = incoming_by_and[gate] + outgoing = outgoing_by_and[gate] + if len(incoming) < 2: + raise ValueError(f"Dummy AND gate {gate!r} must have at least two incoming edges.") + if not outgoing: + raise ValueError(f"Dummy AND gate {gate!r} must have at least one outgoing edge.") + positive = [] + negative = [] + for edge_index in incoming: + source_vertex = next(iter(graph.get_edge(edge_index)[0])) + if _interaction(graph, edge_index) > 0: + positive.append(source_vertex) + else: + negative.append(source_vertex) + for edge_index in outgoing: + if _interaction(graph, edge_index) < 0: + raise ValueError( + f"Dummy AND gate {gate!r} must activate its product; edge {edge_index} has interaction -1." + ) + product = next(iter(graph.get_edge(edge_index)[1])) + add_reaction( + positive, + negative, + product, + [*incoming, edge_index], + ) + + if not reactions: + raise ValueError("CellNOptDAG preprocessing produced no Boolean reactions.") + + dependency_graph = Graph() + species = [vertex for vertex in graph.V if vertex not in and_vertices] + for vertex in species: + dependency_graph.add_vertex(vertex) + + dependency_reactions = [] + for reaction_index, reaction in enumerate(reactions): + for literal in reaction.positive_literals: + dependency_graph.add_edge( + literal, + reaction.product, + interaction=1, + reaction=reaction_index, + ) + dependency_reactions.append(reaction_index) + for literal in reaction.negative_literals: + dependency_graph.add_edge( + literal, + reaction.product, + interaction=-1, + reaction=reaction_index, + ) + dependency_reactions.append(reaction_index) + + vertex_index = {vertex: index for index, vertex in enumerate(dependency_graph.V)} + num_reactions = len(reactions) + positive_rows = [] + positive_columns = [] + negative_rows = [] + negative_columns = [] + product_rows = [] + product_columns = [] + for reaction_index, reaction in enumerate(reactions): + positive_rows.extend([reaction_index] * len(reaction.positive_literals)) + positive_columns.extend(vertex_index[vertex] for vertex in reaction.positive_literals) + negative_rows.extend([reaction_index] * len(reaction.negative_literals)) + negative_columns.extend(vertex_index[vertex] for vertex in reaction.negative_literals) + product_rows.append(vertex_index[reaction.product]) + product_columns.append(reaction_index) + + literal_shape = (num_reactions, dependency_graph.num_vertices) + product_shape = (dependency_graph.num_vertices, num_reactions) + positive_literals = csr_matrix( + ( + np.ones(len(positive_rows)), + (positive_rows, positive_columns), + ), + shape=literal_shape, + ) + negative_literals = csr_matrix( + ( + np.ones(len(negative_rows)), + (negative_rows, negative_columns), + ), + shape=literal_shape, + ) + products = csr_matrix( + ( + np.ones(num_reactions), + (product_rows, product_columns), + ), + shape=product_shape, + ) + return _ReactionNetwork( + graph=dependency_graph, + reactions=tuple(reactions), + dependency_reactions=np.asarray(dependency_reactions, dtype=int), + positive_literals=positive_literals, + negative_literals=negative_literals, + products=products, + ) + + +def _bounded_number( + value: Any, + *, + argument: str, + identifier: Any, + condition: str, + binary: bool, +) -> float: + if isinstance(value, (bool, np.bool_)): + number = float(value) + elif isinstance(value, Real): + number = float(value) + else: + raise TypeError(f"{argument}[{identifier!r}] for condition {condition!r} must be numeric, got {value!r}.") + if not np.isfinite(number): + raise ValueError(f"{argument}[{identifier!r}] for condition {condition!r} must be finite.") + allowed = number in (0, 1) if binary else 0 <= number <= 1 + if not allowed: + domain = "0 or 1" if binary else "between 0 and 1" + raise ValueError(f"{argument}[{identifier!r}] for condition {condition!r} must be {domain}; got {value!r}.") + return number + + +def _cellnopt_data( + graph: BaseGraph, + *, + inputs, + measurements, + inhibitors=None, +) -> Data: + condition_names = validate_condition_keys( + inputs=inputs, + measurements=measurements, + inhibitors=inhibitors, + ) + if not condition_names: + raise ValueError("CellNOptDAG requires at least one named condition.") + if inhibitors is None: + inhibitors = {condition: {} for condition in condition_names} + + graph_vertices = set(graph.V) + features_by_condition = {} + for condition in condition_names: + condition_inputs = require_mapping( + inputs[condition], + argument="inputs", + condition=condition, + ) + condition_measurements = require_mapping( + measurements[condition], + argument="measurements", + condition=condition, + ) + condition_inhibitors = require_mapping( + inhibitors[condition], + argument="inhibitors", + condition=condition, + ) + if not condition_inputs and not condition_inhibitors: + raise ValueError(f"Condition {condition!r} must contain an input or active inhibitor.") + if not condition_measurements: + raise ValueError(f"measurements for condition {condition!r} must not be empty.") + + input_values = {} + measurement_values = {} + active_inhibitors = set() + for vertex, value in condition_inputs.items(): + if vertex not in graph_vertices: + raise ValueError(f"Unknown vertex {vertex!r} in inputs for condition {condition!r}.") + input_values[vertex] = _bounded_number( + value, + argument="inputs", + identifier=vertex, + condition=condition, + binary=True, + ) + for vertex, value in condition_measurements.items(): + if vertex not in graph_vertices: + raise ValueError(f"Unknown vertex {vertex!r} in measurements for condition {condition!r}.") + measurement_values[vertex] = _bounded_number( + value, + argument="measurements", + identifier=vertex, + condition=condition, + binary=False, + ) + for vertex, value in condition_inhibitors.items(): + if vertex not in graph_vertices: + raise ValueError(f"Unknown vertex {vertex!r} in inhibitors for condition {condition!r}.") + inhibitor = _bounded_number( + value, + argument="inhibitors", + identifier=vertex, + condition=condition, + binary=True, + ) + if inhibitor: + active_inhibitors.add(vertex) + + overlap = active_inhibitors & set(input_values) + if overlap: + vertex = next(vertex for vertex in graph.V if vertex in overlap) + raise ValueError( + f"Vertex {vertex!r} cannot be both an input and an active inhibitor in condition {condition!r}." + ) + for vertex in active_inhibitors: + input_values[vertex] = 0.0 + + features = [] + included = set(input_values) | set(measurement_values) + for vertex in (vertex for vertex in graph.V if vertex in included): + is_input = vertex in input_values + is_output = vertex in measurement_values + role = _ROLE_INPUT_OUTPUT if is_input and is_output else (_ROLE_INPUT if is_input else _ROLE_OUTPUT) + feature = { + "id": vertex, + "mapping": "vertex", + "role": role, + "value": (measurement_values[vertex] if is_output else input_values[vertex]), + } + if is_input: + feature["input_value"] = input_values[vertex] + if vertex in active_inhibitors: + feature["intervention"] = "inhibitor" + features.append(feature) + features_by_condition[condition] = features + return data_from_features(features_by_condition) + + +class CellNOptDAG(FlowMethod): + """Infer a shared acyclic Boolean model from multiple conditions. + + The method selects reactions globally and evaluates their Boolean truth in + every condition. A single nonnegative flow has exactly the selected + reaction dependencies as its internal support. Conservation, positive + support, and acyclicity therefore require every selected dependency to lie + on a path from a controlled input or inhibitor to a measured output. + + Dummy vertices named ``AND`` are compiled into one reaction per + product. All operands of such a reaction share one selection variable and + are evaluated conjunctively. + + Args: + lambda_reg: Penalty for every selected reaction. + max_flow: Upper bound for structural flow. By default, the number of + compiled dependency edges is used. + epsilon: Minimum flow on every selected dependency edge. + backend: Optimization backend. + """ + + def __init__( + self, + lambda_reg: float = 1e-3, + max_flow: Optional[float] = None, + epsilon: float = 1.0, + backend: Optional[Backend] = None, + ): + if isinstance(lambda_reg, bool) or not isinstance(lambda_reg, Real): + raise TypeError("lambda_reg must be a finite nonnegative number.") + if not np.isfinite(lambda_reg) or lambda_reg < 0: + raise ValueError("lambda_reg must be a finite nonnegative number.") + if isinstance(epsilon, bool) or not isinstance(epsilon, Real): + raise TypeError("epsilon must be a finite positive number.") + if not np.isfinite(epsilon) or epsilon <= 0: + raise ValueError("epsilon must be a finite positive number.") + if max_flow is not None: + if isinstance(max_flow, bool) or not isinstance(max_flow, Real): + raise TypeError("max_flow must be a finite positive number.") + if not np.isfinite(max_flow) or max_flow <= 0: + raise ValueError("max_flow must be a finite positive number.") + if max_flow < epsilon: + raise ValueError("max_flow must be greater than or equal to epsilon.") + + super().__init__( + lambda_reg=lambda_reg, + reg_varname="reaction_selected", + flow_lower_bound=0, + flow_upper_bound=1, + backend=backend, + ) + self.max_flow = None if max_flow is None else float(max_flow) + self.epsilon = float(epsilon) + self.reactions: tuple[BooleanReaction, ...] = () + self._condition_names: tuple[str, ...] = () + self._biological_num_edges = 0 + self._flow_max = 0.0 + self._dependency_to_reaction = csr_matrix((0, 0)) + self._positive_literals = csr_matrix((0, 0)) + self._negative_literals = csr_matrix((0, 0)) + self._products = csr_matrix((0, 0)) + self._forced_mask = np.empty((0, 0), dtype=bool) + self._forced_values = np.empty((0, 0), dtype=float) + self._measurement_mask = np.empty((0, 0), dtype=bool) + self._measurements = np.empty((0, 0), dtype=float) + + def build( + self, + pkn: BaseGraph, + *, + inputs, + measurements, + inhibitors=None, + ) -> ProblemDef: + """Build a single-condition CellNOpt problem.""" + return self.build_many( + pkn, + inputs={DEFAULT_CONDITION: inputs}, + measurements={DEFAULT_CONDITION: measurements}, + inhibitors=(None if inhibitors is None else {DEFAULT_CONDITION: inhibitors}), + ) + + def build_many( + self, + pkn: BaseGraph, + *, + inputs, + measurements, + inhibitors=None, + ) -> ProblemDef: + """Build a problem for multiple named experimental conditions.""" + data = _cellnopt_data( + pkn, + inputs=inputs, + measurements=measurements, + inhibitors=inhibitors, + ) + return self.build_from_data(pkn, data) + + def preprocess(self, graph: BaseGraph, data: Data): + """Compile reactions, validate condition data, and add flow boundaries.""" + network = _compile_reactions(graph) + if not data.samples: + raise ValueError("CellNOptDAG requires at least one condition.") + + condition_names = tuple(data.samples) + if any(not isinstance(name, str) or not name for name in condition_names): + raise ValueError("CellNOptDAG condition names must be non-empty strings.") + + vertex_index = {vertex: index for index, vertex in enumerate(network.graph.V)} + num_vertices = network.graph.num_vertices + num_conditions = len(condition_names) + forced_mask = np.zeros((num_vertices, num_conditions), dtype=bool) + forced_values = np.zeros((num_vertices, num_conditions), dtype=float) + measurement_mask = np.zeros((num_vertices, num_conditions), dtype=bool) + measurements = np.zeros((num_vertices, num_conditions), dtype=float) + + for condition_index, (condition, sample) in enumerate(data.samples.items()): + condition_inputs = 0 + condition_measurements = 0 + for feature in sample.features: + if feature.mapping != "vertex": + raise ValueError( + f"CellNOptDAG only accepts vertex features; got mapping " + f"{feature.mapping!r} in condition {condition!r}." + ) + if feature.id not in vertex_index: + raise ValueError( + f"Unknown species {feature.id!r} in CellNOptDAG data for " + f"condition {condition!r}; dummy AND vertices cannot be measured " + "or perturbed." + ) + role = feature.data.get("role") + if role not in { + _ROLE_INPUT, + _ROLE_OUTPUT, + _ROLE_INPUT_OUTPUT, + }: + raise ValueError( + f"Vertex feature {feature.id!r} for condition {condition!r} " + "must have role 'input', 'output', or 'input_output'." + ) + index = vertex_index[feature.id] + if role in {_ROLE_INPUT, _ROLE_INPUT_OUTPUT}: + input_value = feature.data.get( + "input_value", + feature.value if role == _ROLE_INPUT else None, + ) + forced_mask[index, condition_index] = True + forced_values[index, condition_index] = _bounded_number( + input_value, + argument="input_value", + identifier=feature.id, + condition=condition, + binary=True, + ) + condition_inputs += 1 + if role in {_ROLE_OUTPUT, _ROLE_INPUT_OUTPUT}: + measurement_mask[index, condition_index] = True + measurements[index, condition_index] = _bounded_number( + feature.value, + argument="measurement", + identifier=feature.id, + condition=condition, + binary=False, + ) + condition_measurements += 1 + if condition_inputs == 0: + raise ValueError(f"Condition {condition!r} must contain at least one input.") + if condition_measurements == 0: + raise ValueError(f"Condition {condition!r} must contain at least one output.") + + input_union = forced_mask.any(axis=1) + output_union = measurement_mask.any(axis=1) + layout = augment_with_boundaries( + network.graph, + inflow_vertices=(vertex for vertex, include in zip(network.graph.V, input_union) if include), + outflow_vertices=(vertex for vertex, include in zip(network.graph.V, output_union) if include), + ) + flow_graph = layout.graph + num_dependencies = network.graph.num_edges + num_reactions = len(network.reactions) + dependency_rows = np.arange(num_dependencies) + dependency_to_reaction = csr_matrix( + ( + np.ones(num_dependencies), + (dependency_rows, network.dependency_reactions), + ), + shape=(flow_graph.num_edges, num_reactions), + ) + + default_max_flow = float(max(num_dependencies, 1)) + flow_max = default_max_flow if self.max_flow is None else self.max_flow + if flow_max < self.epsilon: + raise ValueError(f"max_flow ({flow_max:g}) must be greater than or equal to epsilon ({self.epsilon:g}).") + + self.reactions = network.reactions + self._condition_names = condition_names + self._biological_num_edges = num_dependencies + self._flow_max = float(flow_max) + self._dependency_to_reaction = dependency_to_reaction + self._positive_literals = network.positive_literals + self._negative_literals = network.negative_literals + self._products = network.products + self._forced_mask = forced_mask + self._forced_values = forced_values + self._measurement_mask = measurement_mask + self._measurements = measurements + return flow_graph, data.copy() + + def get_flow_bounds(self, graph: BaseGraph, data: Data): + """Return bounds for the single shared structural flow.""" + return { + "lb": 0, + "ub": self._flow_max, + "n_flows": 1, + "shared_bounds": False, + } + + def create_flow_based_problem( + self, + flow_problem: ProblemDef, + graph: BaseGraph, + data: Data, + ) -> ProblemDef: + """Add vectorized Boolean propagation and shared-flow selection.""" + problem = flow_problem + num_vertices = graph.num_vertices + num_reactions = len(self.reactions) + num_conditions = len(self._condition_names) + condition_ones = np.ones((1, num_conditions)) + + reaction_selected = self.backend.Variable( + "reaction_selected", + (num_reactions,), + vartype=VarType.BINARY, + ) + reaction_active = self.backend.Variable( + "reaction_active", + (num_reactions, num_conditions), + vartype=VarType.BINARY, + ) + vertex_value = self.backend.Variable( + "vertex_value", + (num_vertices, num_conditions), + vartype=VarType.BINARY, + ) + + dependency_selected_all = self.backend.Constant(self._dependency_to_reaction) @ reaction_selected + dependency_selected = dependency_selected_all[: self._biological_num_edges] + flow = problem.expr.flow + biological_flow = flow[: self._biological_num_edges] + # The shared flow and shared reaction selection have exactly the same + # support on biological dependencies. Boundary flow remains free to + # choose which controlled species and measurements connect that support. + problem += biological_flow >= self.epsilon * dependency_selected + problem += biological_flow <= self._flow_max * dependency_selected + + problem.register("_dependency_selected_all", dependency_selected_all) + self.backend.Acyclic( + graph, + problem, + indicator_positive_var_name="_dependency_selected_all", + ) + + positive_literals = self.backend.Constant(self._positive_literals) + negative_literals = self.backend.Constant(self._negative_literals) + products = self.backend.Constant(self._products) + negative_count = np.asarray(self._negative_literals.sum(axis=1)).reshape(-1, 1) + literal_count = np.asarray((self._positive_literals + self._negative_literals).sum(axis=1)).reshape(-1, 1) + negative_count = negative_count @ condition_ones + literal_count = literal_count @ condition_ones + # S[r, c] counts true literals of reaction r in condition c. With k + # literals, the three inequalities below impose + # Z[r, c] = Y[r] AND (S[r, c] == k[r]) without a big-M constant. + literal_satisfaction = positive_literals @ vertex_value + negative_count - negative_literals @ vertex_value + selected_by_condition = reaction_selected.reshape((num_reactions, 1)) @ condition_ones + + problem += reaction_active <= selected_by_condition + problem += reaction_active.multiply(literal_count) <= literal_satisfaction + problem += reaction_active >= selected_by_condition + literal_satisfaction - literal_count + + producing_reactions = products @ reaction_active + producer_count = np.asarray(self._products.sum(axis=1)).reshape(-1, 1) + producer_count = producer_count @ condition_ones + free_mask = (~self._forced_mask).astype(float) + # A non-intervened species is the OR of its active producing reactions. + # Interventions mask only this product relation; upstream reaction truth + # remains intact when a product is experimentally forced to zero. + problem += vertex_value.multiply(free_mask) <= producing_reactions.multiply(free_mask) + problem += producing_reactions.multiply(free_mask) <= vertex_value.multiply(producer_count * free_mask) + problem += vertex_value.multiply(self._forced_mask.astype(float)) == self._forced_values + + error_coefficients = self._measurement_mask.astype(float) * (1 - 2 * self._measurements) + error_constant = float(np.sum(self._measurement_mask * self._measurements)) + # For binary x and measurement m in [0, 1], + # |x - m| = m + (1 - 2m)x exactly. + measurement_error = vertex_value.multiply(error_coefficients).sum() + error_constant + problem.add_objective( + measurement_error, + name="measurement_error", + ) + + problem.register("dependency_selected", dependency_selected) + problem.register("literal_satisfaction", literal_satisfaction) + problem.register("dag_layer", problem.expr._dag_layer) + return problem + + @staticmethod + def name() -> str: + """Return the method name.""" + return "CellNOptDAG" + + @staticmethod + def description() -> str: + """Return a short method description.""" + return "Shared acyclic Boolean-reaction inference with structural flow connectivity" diff --git a/corneto/methods/signaling/cellnopt_ilp.py b/corneto/methods/signaling/cellnopt_ilp.py deleted file mode 100644 index 942c66144..000000000 --- a/corneto/methods/signaling/cellnopt_ilp.py +++ /dev/null @@ -1,620 +0,0 @@ -"""CellNOpt integer-linear programming method and visualization helpers.""" - -import re -from typing import Literal, Optional - -import numpy as np - -__all__ = [ - "cellnoptILP", - "cno_style", - "expand_graph_for_flows", - "plot_data", - "plot_fitness", -] - -import corneto as cn -from corneto.backend._base import EXPR_NAME_FLOW - - -def clip_quantiles(arr, q): - """Clip an array to its lower and upper quantiles.""" - if q < 0 or q > 1: - raise ValueError(f"Clipping value must be between 0 and 1, got {q}") - # compute the quantiles at clipping and 1-clipping and clip the flow - q = np.quantile(arr, [q, 1 - q]) - return np.clip(arr, q[0], q[1]) - - -def cno_style( - P, - max_edge_width: float = 5, - min_edge_width: float = 0.25, - flow_name: str = EXPR_NAME_FLOW, - positive_color: str = "dodgerblue4", - negative_color: str = "firebrick4", - zero_flow_threshold: float = 1e-6, - scale: Optional[Literal["log", "std"]] = "log", - clip_quantil: Optional[float] = 0.05, - iexp=0, -): - """Return graph-plotting attributes derived from a CellNOpt solution.""" - flow = np.array(P.expr[flow_name].value)[:, iexp] - flow[np.abs(flow) < zero_flow_threshold] = 0 - if scale is not None: - if scale == "log": - flow = np.log10(np.abs(flow) + 1e-6) * np.sign(flow) - elif scale == "std": - flow = flow / np.std(flow) - else: - raise ValueError(f"Unknown normalization method: {scale}") - if clip_quantil is not None: - flow = clip_quantiles(flow, clip_quantil) - max_flow = max(np.max(np.abs(flow)), 1e-6) - edge_attrs = dict() - for i, v in enumerate(flow): - # Apply threshold edge width - if abs(v) > 0: - edge_width = max_edge_width - else: - edge_width = min_edge_width - if scale is not None: - edge_width = min_edge_width + (max_edge_width - min_edge_width) * abs(v / max_flow) - # bound = P.expr[flow_name].ub[i] if v >= 0 else P.expr[flow_name].lb[i] - edge_attrs[i] = {"penwidth": str(edge_width)} - if flow[i] > 0: - edge_attrs[i]["color"] = positive_color - elif flow[i] < 0: - edge_attrs[i]["color"] = negative_color - else: - edge_attrs[i]["color"] = "black" - return edge_attrs - - -def get_interactions(G): - """Get the sign of interactions from the graph G. I in [1, -1]""" - return np.array(G.get_attr_from_edges("interaction", 1)) - - -def get_AND_gate_nodes(G): - """Get the indices of nodes that represent AND gates in the graph G. - - Parameters: - - G (Graph): The input graph. - - Returns: - - np.array: An array containing the indices of nodes that represent AND gates. - """ - # find AND gates with regular expression: AND[0+9]+ - pattern = re.compile(r"^(AND|And|and)[0-9]+$") - V_is_and = [bool(pattern.match(v)) for v in G.V] - - return np.array(V_is_and) - - -def get_incidence_matrices_of_edges(G, as_dataframe=False): - """Get the mapping matrices A, At, Ah from the graph G. - - Parameters: - - G: The graph object. - - as_dataframe: Whether to return the matrices as pandas DataFrames. Default is False. - - Returns: - - At: The tail-incidence matrix. - - Ah: The head-incidence matrix. - """ - A = G.vertex_incidence_matrix().astype(int) # V x E - At = np.clip(A, 0, 1) # V x E, 1 if vertex appears as tail of edge - Ah = np.clip(-A, 0, 1) # V x E, 1 if vertex appears as head of edge - - if as_dataframe: - import pandas as pd - - Ah = pd.DataFrame(Ah, index=G.V, columns=G.E) - At = pd.DataFrame(At, index=G.V, columns=G.E) - - return At, Ah - - -def get_egdes_with_head(G): - """Get the indices of edges with a head node. - - Parameters: - G (graph): The input graph. - - Returns: - edges_with_head (array): An array containing the indices of edges with a head node. - """ - _At, Ah = get_incidence_matrices_of_edges(G) - edges_with_head = np.flatnonzero(np.sum(np.abs(Ah), axis=0) > 0) - return edges_with_head - - -def get_inhibited_nodes(G, exp_list): - """Returns an array, with shape = (len(G.V), len(exp_list)), where each column is a boolean array - indicating if the node is inhibited in the corresponding experiment. - """ - V_is_inhibited = np.full((len(G.V), len(exp_list)), False) - - for exp, iexp in zip(exp_list, range(len(exp_list))): - if "inhibition" not in exp_list[exp]: - continue - i_nodes = list(exp_list[exp]["inhibition"].keys()) - V_is_inhibited[:, iexp] = np.array([v in i_nodes for v in G.V]) - return V_is_inhibited - - -def presolve_report(G, exp_list): - At, Ah = get_incidence_matrices_of_edges(G, as_dataframe=True) - interaction = get_interactions(G) - edges_with_head = get_egdes_with_head(G) - V_is_and = get_AND_gate_nodes(G) - - print("Vertex order:") - print(G.V) - print("AND gates:") - print(V_is_and) - print("Tails of interactions:") - print(At) - print("Head of interactions:") - print(Ah) - print("Sign of interactions:") - print(interaction) - print("Edges with head:") - print(edges_with_head) - - -def expand_graph_for_flows(G, exp_list): - """Expand the graph G with the perturbations and measurements from the experiments in exp_list.""" - G1 = G.copy() - output_names = list({key for exp in exp_list.values() for key in exp["output"].keys()}) - input_names = list({key for exp in exp_list.values() for key in exp["input"].keys()}) - - output_names = list(set(output_names)) - input_names = list(set(input_names)) - - for node in output_names: - G1.add_edge(node, ()) - for node in input_names: - G1.add_edge((), node) - - return G1 - - -def check_exp_graph_consistency(G, exp_list): - """Check if the experiments are consistent with the graph G.""" - for exp in exp_list: - for node in exp_list[exp]["input"]: - if node not in G.V: - raise ValueError(f"Node {node} in experiment {exp} is not in the graph.") - for node in exp_list[exp]["output"]: - if node not in G.V: - raise ValueError(f"Node {node} in experiment {exp} is not in the graph.") - if "inhibition" in exp_list[exp]: - for node in exp_list[exp]["inhibition"]: - if node not in G.V: - raise ValueError(f"Node {node} in experiment {exp} is not in the graph.") - - -def cellnoptILP(G, exp_list, solver=None, alpha_flow=1e-3, verbose=False, backend=None): - """Create and solves the ILP model for the given graph G and the list of experiments exp_list. - - Parameters: - - G: The graph representing the network. - - exp_list: The list of experiments. - - solver: The solver to use for solving the ILP problem. Default is None. - - alpha_flow: The weight of the penalty of flow in the objective. Default is 1e-3. - - verbose: Whether to print verbose output. Default is False. - - Returns: - - P: The ILP model. - """ - if backend is None: - backend = cn.DEFAULT_BACKEND - check_exp_graph_consistency(G, exp_list) - - At, Ah = get_incidence_matrices_of_edges(G) - interaction = get_interactions(G) - edges_with_head = get_egdes_with_head(G) - edges_with_head = np.flatnonzero(np.sum(np.abs(Ah), axis=0) > 0) - V_is_and = get_AND_gate_nodes(G) - and_idx = np.flatnonzero(V_is_and) - V_is_inhibited = get_inhibited_nodes(G, exp_list) - - # let's start with acyclic flow - P = backend.AcyclicFlow(G) - - # vertex value is binary (0 and 1) - V = backend.Variable("vertex_value", (G.num_vertices, len(exp_list)), vartype=cn.VarType.BINARY) - # edge activation is also binary: - Eact = backend.Variable("edge_activates", (G.num_edges, len(exp_list)), vartype=cn.VarType.BINARY) - - M = 100 # a large number, so sum(incoming edges)/M is always less than 1 - - # Dummy variable for the linearization of the absolute deviation objective function - Z = backend.Variable("dummy", (G.num_vertices, len(exp_list)), vartype=cn.VarType.CONTINUOUS) - P += Z >= 0 - - # some basic constraints - P += V >= 0 - P += V <= 1 - - # Rule 1: Edge can be activated only if they carry flow - for exp, iexp in zip(exp_list, range(len(exp_list))): - P += Eact[:, iexp] <= P.expr.with_flow - - # Rule 2: Edges take the upstream value for a positive sign and its - # complement for a negative sign. - for exp, iexp in zip(exp_list, range(len(exp_list))): - # this should keep Eact in [0,1] interval: - - # signed value of source/head node for an edge: - V_head = (Ah.T @ V)[edges_with_head, iexp].multiply((interaction[edges_with_head] > 0).astype(int)) + ( - 1 - (Ah.T @ V)[edges_with_head, iexp] - ).multiply((interaction[edges_with_head] < 0).astype(int)) - # an edge is active if there is flow AND there head node is also active - # logical AND is translated to ILP as: - # y >= x1 + x2 - 1 ; y <= x1 ; y <= x2 - P += Eact[edges_with_head, iexp] >= P.expr.with_flow[edges_with_head] + V_head - 1 - P += Eact[edges_with_head, iexp] <= V_head - # The with-flow upper bound is already enforced by Rule 1. - - # Rule 3: propagate the active edges to the vertices - - # This is for general nodes that are not AND gates - # - for exp, iexp in zip(exp_list, range(len(exp_list))): - is_regular_node = np.logical_and(~V_is_and, ~V_is_inhibited[:, iexp]) - is_regular_node = np.flatnonzero(is_regular_node) - - P += ( - V[is_regular_node, iexp] >= ((At @ Eact) / M)[is_regular_node, iexp] - ) # when there is at least one active edge, the vertex becomes 1 (larger than someValue/M) - P += ( - V[is_regular_node, iexp] <= (At @ Eact)[is_regular_node, iexp] - ) # but it has an upper constraint, so it takes 0 when all input are 0 - if V_is_inhibited[:, iexp].any(): - inh_idx = np.flatnonzero(V_is_inhibited[:, iexp]) - P += V[inh_idx, iexp] == np.zeros(inh_idx.size) - - # AND relation expressed as follows: - # - we only define these constraints for the AND gates - # - we count the sum of flows (selected edges) and sum of activated edges - # - if flow equals incoming-edge activation, all edges are activated and so - # is the vertex; - # - a second constraint prevents activation when no flow is present. - if V_is_and.any(): - # and_idx = np.flatnonzero(V_is_and) - sum_of_flow = At[and_idx, :] @ P.expr.with_flow - sum_of_edge_activation = At[and_idx, :] @ Eact - - # (sum_of_flow - sum_of_edge_activation)/M is always less than 1 and it is 0 if all edges are activated. - for exp, iexp in zip(exp_list, range(len(exp_list))): - P += V[and_idx, iexp] <= 1 - (sum_of_flow - sum_of_edge_activation[:, iexp]) / M - P += V[and_idx, iexp] <= sum_of_flow - - P.register("sum_of_flow", sum_of_flow) - P.register("sum_of_edge_activation", sum_of_edge_activation) - - for exp, iexp in zip(exp_list, range(len(exp_list))): - # activation: - p_nodes = list(exp_list[exp]["input"].keys()) - p_values = list(exp_list[exp]["input"].values()) - p_nodes_positions = np.array([G.V.index(key) for key in p_nodes]) - - P += V[p_nodes_positions, iexp] == p_values - - # measurements: - m_nodes = list(exp_list[exp]["output"].keys()) - m_values = np.array(list(exp_list[exp]["output"].values())) - m_nodes_positions = np.array([G.V.index(key) for key in m_nodes]) - - # linearization of the ABS function: https://lpsolve.sourceforge.net/5.1/absolute.htm - P += V[m_nodes_positions, iexp] - m_values <= Z[m_nodes_positions, iexp] - P += -(V[m_nodes_positions, iexp] - m_values) <= Z[m_nodes_positions, iexp] - - P.add_objectives(sum(Z[m_nodes_positions, iexp])) - - P.add_objectives(alpha_flow * sum(P.expr.with_flow)) - - P.solve(solver=solver, verbosity=verbose) - - return P - - -def report_solution_tables(G, exp_list, P): - import pandas as pd - - for iexp in range(len(exp_list)): - print("--------- iexp: ", iexp, " ---------") - print(pd.DataFrame({"V": G.V, "value": P.expr.vertex_value.value[:, iexp]})) - print( - pd.DataFrame( - { - "E": G.E, - "flow": P.expr.with_flow.value, - "Eact": P.expr.edge_activates.value[:, iexp], - } - ) - ) - - -def plot_solution_network_active_edges(G, P, iexp): - G.plot(custom_edge_attr=cno_style(P, flow_name="edge_activates", scale=None, iexp=iexp)) - - -def plot_fitness(G, exp_list, P, measured_only=False, **kwargs): - """Plot the fitness of the model simulation vs measurements. - - PARAMETERS: - - G: corneto.Graph object - - exp_list: dictionary of experiments - - P: solution of the ILP model - - measured_only: if True, plot only the measured nodes, otherwise plot all nodes - - **kwargs arguments are passed to subplots and figures of matplotlib - - TODO: there are some assumptions, like the first experiment is the reference experiment and it is called 'exp0' - """ - import matplotlib.pyplot as plt - - N_exps = len(exp_list) - - # Ensure that all experiments have the input and output variables - for exp in exp_list.values(): - if "input" not in exp: - raise ValueError("Input not found in experiment") - if "output" not in exp: - raise ValueError("Output not found in experiment") - - # Collect the input and output variables - input_matrix, input_vars = collect_field_into_matrix(exp_list, "input") - _output_matrix, _output_vars = collect_field_into_matrix(exp_list, "output") - - # Check if inhibition is present in any of the experiments - inhibition_present = any("inhibition" in exp for exp in exp_list.values()) - if inhibition_present: - inhibition_matrix, inhibition_vars = collect_field_into_matrix(exp_list, "inhibition") - perturbation_matrix = np.hstack((input_matrix, inhibition_matrix)) - perturbation_vars = input_vars + inhibition_vars - else: - perturbation_matrix = input_matrix - perturbation_vars = input_vars - - # Create the figure - # Set colors: input colors are blue, inhibition colors are red - perturbation_colors = ["blue"] * len(input_vars) - if inhibition_present: - perturbation_colors += ["red"] * len(inhibition_vars) - - N_nodes = len(G.V) - output_names = list({key for exp in exp_list.values() for key in exp["output"].keys()}) - - # depending on the flag measured_only, we can plot only the measured nodes or all nodes - if measured_only: - fig, axs = plt.subplots(N_exps - 1, len(output_names) + 1, squeeze=False, **kwargs) - else: - fig, axs = plt.subplots(N_exps - 1, N_nodes + 1, squeeze=False, **kwargs) - - fig.tight_layout(pad=0.0) - - # Adjust the space between subplots - plt.subplots_adjust(wspace=0.1, hspace=0.1) - - for exp, iexp in zip(exp_list, range(N_exps)): - if iexp == 0: - continue - - if measured_only: - for imarker in range(len(output_names)): - # output_names[imarker] is the name of the output node, find the position in the graph - imarker_inG = G.V.index(output_names[imarker]) - - axs[iexp - 1, imarker].plot( - [0, 10], - [ - P.expr.vertex_value.value[imarker_inG, 0], - min(P.expr.vertex_value.value[imarker_inG, iexp], 1), - ], - "bo-", - label=G.V[imarker_inG], - ) - - if G.V[imarker_inG] in exp_list[exp]["output"].keys(): - axs[iexp - 1, imarker].plot( - [0, 10], - [ - exp_list["exp0"]["output"][G.V[imarker_inG]], - exp_list[exp]["output"][G.V[imarker_inG]], - ], - "ro-", - ) - axs[iexp - 1, imarker].set_ylim([-0.05, 1.1]) - if iexp == 1: - axs[iexp - 1, imarker].set_title(output_names[imarker]) - if iexp != N_exps - 1: - axs[iexp - 1, imarker].set_xticks([]) - if imarker == 0: - axs[iexp - 1, imarker].set_ylabel(f"Exp. {iexp}") - else: - axs[iexp - 1, imarker].set_yticks([]) - else: - for imarker in range(N_nodes): - axs[iexp - 1, imarker].plot( - [0, 10], - [ - P.expr.vertex_value.value[imarker, 0], - min(P.expr.vertex_value.value[imarker, iexp], 1), - ], - "bo-", - label=G.V[imarker], - # color="blue", - # linestyle="o-", - ) - - if G.V[imarker] in exp_list[exp]["output"].keys(): - axs[iexp - 1, imarker].plot( - [0, 10], - [ - exp_list["exp0"]["output"][G.V[imarker]], - exp_list[exp]["output"][G.V[imarker]], - ], - "ro-", - ) - axs[iexp - 1, imarker].set_ylim([-0.05, 1.1]) - if iexp == 1: - axs[iexp - 1, imarker].set_title(G.V[imarker]) - if iexp != N_exps - 1: - axs[iexp - 1, imarker].set_xticks([]) - if imarker == 0: - axs[iexp - 1, imarker].set_ylabel(f"Exp. {iexp}") - else: - axs[iexp - 1, imarker].set_yticks([]) - # Plot perturbation - if measured_only: - plot_location = len(output_names) - else: - plot_location = N_nodes - - axs[iexp - 1, plot_location].bar( - range(len(perturbation_vars)), - perturbation_matrix[iexp], - color=perturbation_colors, - ) - if iexp == N_exps - 1: - axs[iexp - 1, plot_location].set_xticks(range(len(perturbation_vars))) - axs[iexp - 1, plot_location].set_xticklabels(perturbation_vars, rotation=45) - else: - # No xtick label - axs[iexp - 1, plot_location].set_xticks([]) - - axs[iexp - 1, plot_location].set_ylim([-0.01, 1.1]) - axs[iexp - 1, plot_location].set_yticks([]) - if iexp == 1: - axs[iexp - 1, plot_location].set_title("Pert.") - - plt.show() - - -def collect_field_into_matrix(experiments, field_name="input"): - """Collects the field_name values into matrix. - - Collects the field_name values (input, inhibition etc) from a dictionary - of experiments and returns them as a numpy array. - - PARAMETERS: - - experiments: dictionary of experiments containing input values - - field_name: name of the field to collect (default: 'input') - - Returns: - - input_matrix: numpy array of input values - - input_vars: list of unique input variable names - - """ - # Collect all unique input names - input_vars = set() - for exp in experiments.values(): - # ensure field_name exists - if field_name not in exp: - raise ValueError("Field name (" + field_name + ") not found in experiment") - input_vars.update(exp[field_name].keys()) - - input_vars = sorted(input_vars) # Sorting to keep a consistent order - data = [] - - # Collect input values for each experiment - for exp in experiments.values(): - row = [exp[field_name].get(var, 0) for var in input_vars] - data.append(row) - - # Convert the data to a numpy array - input_matrix = np.array(data) - - return input_matrix, input_vars - - -def plot_data(exp_list): - """Plot the data. - - PARAMETERS: - - exp_list: dictionary of experiments - - """ - import matplotlib.pyplot as plt - - N_exps = len(exp_list) - - # Ensure that all experiments have the input and output variables - for exp in exp_list.values(): - if "input" not in exp: - raise ValueError("Input not found in experiment") - if "output" not in exp: - raise ValueError("Output not found in experiment") - - # Collect the input and output variables - input_matrix, input_vars = collect_field_into_matrix(exp_list, "input") - output_matrix, output_vars = collect_field_into_matrix(exp_list, "output") - - # Check if inhibition is present in any of the experiments - inhibition_present = any("inhibition" in exp for exp in exp_list.values()) - if inhibition_present: - inhibition_matrix, inhibition_vars = collect_field_into_matrix(exp_list, "inhibition") - perturbation_matrix = np.hstack((input_matrix, inhibition_matrix)) - perturbation_vars = input_vars + inhibition_vars - else: - perturbation_matrix = input_matrix - perturbation_vars = input_vars - - # Create the figure - # Set colors: input colors are blue, inhibition colors are red - perturbation_colors = ["blue"] * len(input_vars) - if inhibition_present: - perturbation_colors += ["red"] * len(inhibition_vars) - - fig, axs = plt.subplots(N_exps - 1, len(output_vars) + 1, squeeze=False) - - fig.tight_layout(pad=0.0) - # Adjust the space between subplots - plt.subplots_adjust(wspace=0.1, hspace=0.1) - - for exp, iexp in zip(exp_list, range(N_exps)): - if iexp == 0: - continue - - for imarker in range(len(output_vars)): - # output_names[imarker] is the name of the output node, find the position in the graph - imarker_name = output_vars[imarker] - - axs[iexp - 1, imarker].plot( - [0, 10], - [output_matrix[0, imarker], output_matrix[iexp, imarker]], - "ro-", - ) - axs[iexp - 1, imarker].set_ylim([-0.01, 1.1]) - - if iexp == 1: - axs[iexp - 1, imarker].set_title(imarker_name) - if iexp != N_exps - 1: - axs[iexp - 1, imarker].set_xticks([]) - if imarker == 0: - axs[iexp - 1, imarker].set_ylabel(f"Exp. {iexp}") - else: - axs[iexp - 1, imarker].set_yticks([]) - - # Plot perturbation - axs[iexp - 1, len(output_vars)].bar( - range(len(perturbation_vars)), - perturbation_matrix[iexp], - color=perturbation_colors, - ) - if iexp == N_exps - 1: - axs[iexp - 1, len(output_vars)].set_xticks(range(len(perturbation_vars))) - axs[iexp - 1, len(output_vars)].set_xticklabels(perturbation_vars, rotation=45) - else: - # No xtick label - axs[iexp - 1, len(output_vars)].set_xticks([]) - - axs[iexp - 1, len(output_vars)].set_ylim([-0.01, 1.1]) - axs[iexp - 1, len(output_vars)].set_yticks([]) - if iexp == 1: - axs[iexp - 1, len(output_vars)].set_title("Pert.") - plt.show() diff --git a/corneto/methods/signaling/cellnopt_plotting.py b/corneto/methods/signaling/cellnopt_plotting.py new file mode 100644 index 000000000..ca2df369e --- /dev/null +++ b/corneto/methods/signaling/cellnopt_plotting.py @@ -0,0 +1,880 @@ +"""Visualization utilities for :class:`~corneto.methods.signaling.CellNOptDAG`.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal, Optional, Sequence, Union + +import numpy as np + +from corneto._plotting import _scaled_magnitudes +from corneto.graph import Graph +from corneto.methods.signaling.cellnopt_dag import CellNOptDAG + +__all__ = ["plot_cellnopt_fit", "plot_cellnopt_model"] + +_STIMULUS_COLOR = "#9ACD32" +_MEASUREMENT_COLOR = "#ADD8E6" +_INHIBITOR_COLOR = "#FF6846" +_POSITIVE_COLOR = "#222222" +_NEGATIVE_COLOR = "#C43C39" +_INACTIVE_POSITIVE_COLOR = "#A8A8A8" +_INACTIVE_NEGATIVE_COLOR = "#E3A19F" +_UNSELECTED_POSITIVE_COLOR = "#D5D5D5" +_UNSELECTED_NEGATIVE_COLOR = "#F0CECD" + + +@dataclass(frozen=True) +class _CellNOptPlotData: + conditions: tuple[str, ...] + vertices: tuple[Any, ...] + selected: np.ndarray + active: np.ndarray + states: np.ndarray + flow_by_reaction: tuple[np.ndarray, ...] + measurement_mask: np.ndarray + measurements: np.ndarray + input_mask: np.ndarray + input_values: np.ndarray + inhibitor_mask: np.ndarray + + +@dataclass(frozen=True) +class _FitSeries: + """Observed and predicted responses on an explicit time axis. + + CellNOptDAG currently infers one steady-state endpoint per condition, so + extraction produces a singleton axis labelled ``Endpoint``. Keeping time + as a real array dimension lets a future time-resolved formulation reuse + the renderer without assigning scientific meaning to series identity. + """ + + times: np.ndarray + time_labels: tuple[str, ...] + observed: np.ndarray + measured: np.ndarray + predicted: np.ndarray + + +@dataclass(frozen=True) +class _ModelPlotSpec: + graph: Graph + vertex_attributes: dict[Any, dict[str, str]] + edge_attributes: dict[int, dict[str, str]] + graph_attributes: dict[str, str] + node_attributes: dict[str, str] + + +def _expression_values(problem: Any, name: str, shape: tuple[int, ...]) -> np.ndarray: + if problem is None: + raise ValueError("CellNOptDAG has not been built. Build and solve the method before plotting.") + try: + expression = problem.expr[name] + except (KeyError, TypeError, AttributeError): + try: + expression = getattr(problem.expr, name) + except AttributeError as exc: + raise ValueError(f"CellNOpt solution does not contain expression {name!r}.") from exc + value = getattr(expression, "value", None) + if value is None: + raise ValueError(f"CellNOpt expression {name!r} has no value. Solve a feasible problem before plotting.") + array = np.asarray(value, dtype=float) + try: + array = array.reshape(shape) + except ValueError as exc: + raise ValueError(f"CellNOpt expression {name!r} has shape {array.shape}; expected {shape}.") from exc + if not np.all(np.isfinite(array)): + raise ValueError(f"CellNOpt expression {name!r} contains non-finite values.") + return array + + +def _validate_binary_values(values: np.ndarray, name: str, tolerance: float = 1e-5) -> np.ndarray: + if np.any(values < -tolerance) or np.any(values > 1 + tolerance): + raise ValueError(f"CellNOpt expression {name!r} contains values outside the binary domain.") + return values >= 0.5 + + +def _extract_plot_data(method: CellNOptDAG, problem: Any = None) -> _CellNOptPlotData: + if not isinstance(method, CellNOptDAG): + raise TypeError("method must be a CellNOptDAG instance.") + if problem is None: + problem = method.problem + conditions = tuple(method._condition_names) + vertices = tuple(method.processed_graph.V) if method.processed_graph is not None else () + if not conditions or not vertices or not method.reactions: + raise ValueError("CellNOptDAG has not been built. Build and solve the method before plotting.") + + num_reactions = len(method.reactions) + num_conditions = len(conditions) + num_vertices = len(vertices) + selected_values = _expression_values(problem, "reaction_selected", (num_reactions,)) + active_values = _expression_values(problem, "reaction_active", (num_reactions, num_conditions)) + state_values = _expression_values(problem, "vertex_value", (num_vertices, num_conditions)) + flow_values = _expression_values(problem, "flow", tuple(problem.expr.flow.shape)).reshape(-1) + if flow_values.size < method._biological_num_edges: + raise ValueError("CellNOpt flow vector is shorter than the compiled biological dependency graph.") + + selected = _validate_binary_values(selected_values, "reaction_selected") + active = _validate_binary_values(active_values, "reaction_active") + states = _validate_binary_values(state_values, "vertex_value") + + dependency_flow = flow_values[: method._biological_num_edges] + flow_by_reaction = [] + cursor = 0 + for reaction in method.reactions: + next_cursor = cursor + len(reaction.literals) + flow_by_reaction.append(dependency_flow[cursor:next_cursor].copy()) + cursor = next_cursor + if cursor != method._biological_num_edges: + raise ValueError("Compiled reactions do not align with CellNOpt dependency-flow variables.") + + input_mask = np.zeros((num_vertices, num_conditions), dtype=bool) + input_values = np.zeros((num_vertices, num_conditions), dtype=float) + inhibitor_mask = np.zeros((num_vertices, num_conditions), dtype=bool) + vertex_index = {vertex: index for index, vertex in enumerate(vertices)} + for condition_index, condition in enumerate(conditions): + sample = method.processed_data.samples[condition] + for feature in sample.features: + index = vertex_index[feature.id] + role = feature.data.get("role") + if role in {"input", "input_output"}: + input_mask[index, condition_index] = True + input_values[index, condition_index] = float(feature.data.get("input_value", feature.value)) + if feature.data.get("intervention") == "inhibitor": + inhibitor_mask[index, condition_index] = True + + return _CellNOptPlotData( + conditions=conditions, + vertices=vertices, + selected=selected, + active=active, + states=states, + flow_by_reaction=tuple(flow_by_reaction), + measurement_mask=np.asarray(method._measurement_mask, dtype=bool), + measurements=np.asarray(method._measurements, dtype=float), + input_mask=input_mask, + input_values=input_values, + inhibitor_mask=inhibitor_mask, + ) + + +def _condition_index( + data: _CellNOptPlotData, + condition: Optional[Union[int, str]], +) -> Optional[int]: + if condition is None: + return None + if isinstance(condition, int): + if condition < 0 or condition >= len(data.conditions): + raise ValueError(f"condition index {condition} out of range for {len(data.conditions)} conditions.") + return condition + if condition not in data.conditions: + raise ValueError(f"Unknown condition {condition!r}; expected one of {list(data.conditions)!r}.") + return data.conditions.index(condition) + + +def _reaction_text(reaction: Any) -> str: + literals = [str(vertex) for vertex in reaction.positive_literals] + literals.extend(f"NOT {vertex}" for vertex in reaction.negative_literals) + return f"{' AND '.join(literals)} -> {reaction.product}" + + +def _node_roles( + data: _CellNOptPlotData, + condition_index: Optional[int], +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + if condition_index is None: + inputs = data.input_mask.any(axis=1) + outputs = data.measurement_mask.any(axis=1) + inhibitors = data.inhibitor_mask.any(axis=1) + else: + inputs = data.input_mask[:, condition_index] + outputs = data.measurement_mask[:, condition_index] + inhibitors = data.inhibitor_mask[:, condition_index] + return inputs, outputs, inhibitors + + +def _merge_attributes( + generated: dict[Any, dict[str, str]], + supplied: Optional[dict[Any, dict[str, str]]], +) -> dict[Any, dict[str, str]]: + merged = {key: dict(value) for key, value in generated.items()} + for key, attrs in (supplied or {}).items(): + merged.setdefault(key, {}).update({str(name): str(value) for name, value in attrs.items()}) + return merged + + +def _build_cellnopt_model_plot( + method: CellNOptDAG, + problem: Any = None, + *, + condition: Optional[Union[int, str]] = None, + show_unselected: bool = False, + show_inactive: bool = True, + width_by: Literal["selection", "flow"] = "selection", +) -> _ModelPlotSpec: + if width_by not in {"selection", "flow"}: + raise ValueError("width_by must be 'selection' or 'flow'.") + data = _extract_plot_data(method, problem) + condition_index = _condition_index(data, condition) + + included_reactions = [] + segment_flows = [] + for reaction_index, reaction in enumerate(method.reactions): + selected = bool(data.selected[reaction_index]) + active = selected if condition_index is None else bool(data.active[reaction_index, condition_index]) + include = selected or show_unselected + if condition_index is not None and selected and not active and not show_inactive: + include = False + if not include: + continue + included_reactions.append((reaction_index, reaction, selected, active)) + flows = data.flow_by_reaction[reaction_index] + segment_flows.extend(flows.tolist()) + if len(reaction.literals) > 1: + segment_flows.append(float(np.sum(flows))) + + flow_scale = ( + _scaled_magnitudes( + np.asarray(segment_flows, dtype=float), + zero_threshold=1e-9, + scale="log", + clip_quantile=0.0, + ) + if width_by == "flow" + else np.empty(0) + ) + flow_scale_cursor = 0 + + graph = Graph() + included_species = set() + for _, reaction, _, _ in included_reactions: + included_species.update(reaction.literals) + included_species.add(reaction.product) + inputs, outputs, inhibitors = _node_roles(data, condition_index) + experimental_species = { + vertex for vertex, is_experimental in zip(data.vertices, inputs | outputs | inhibitors) if is_experimental + } + included_species.update(experimental_species) + for vertex in data.vertices: + if vertex in included_species: + graph.add_vertex(vertex) + + existing_names = {str(vertex) for vertex in data.vertices} + and_nodes = {} + for reaction_index, reaction, _, _ in included_reactions: + if len(reaction.literals) <= 1: + continue + candidate = f"__cellnopt_and_{reaction_index}" + while candidate in existing_names: + candidate = f"_{candidate}" + existing_names.add(candidate) + and_nodes[reaction_index] = candidate + graph.add_vertex(candidate) + + vertex_attributes: dict[Any, dict[str, str]] = {} + vertex_index = {vertex: index for index, vertex in enumerate(data.vertices)} + for vertex in included_species: + index = vertex_index[vertex] + is_input = bool(inputs[index]) + is_output = bool(outputs[index]) + is_inhibitor = bool(inhibitors[index]) + is_stimulus = is_input and not is_inhibitor + if is_inhibitor: + fillcolor = _INHIBITOR_COLOR + role = "inhibitor target" + elif is_stimulus: + fillcolor = _STIMULUS_COLOR + role = "stimulus" + elif is_output: + fillcolor = _MEASUREMENT_COLOR + role = "measurement" + else: + fillcolor = "white" + role = "internal species" + attrs = { + "shape": "box", + "style": "rounded,filled", + "fillcolor": fillcolor, + "color": "#4C4C4C", + "fontcolor": "#111111", + "label": str(vertex), + "tooltip": role, + "margin": "0.08,0.05", + } + if is_stimulus and is_output: + attrs["peripheries"] = "2" + attrs["color"] = "#4C78A8" + attrs["tooltip"] = "stimulus and measurement" + elif is_inhibitor and is_output: + attrs["peripheries"] = "2" + attrs["color"] = "#4C78A8" + attrs["tooltip"] = "inhibitor target and measurement" + if condition_index is not None: + value = int(data.states[index, condition_index]) + attrs["penwidth"] = "2.5" if value else "1.0" + attrs["tooltip"] += f"; state={value}" + vertex_attributes[str(vertex)] = attrs + for reaction_index, node in and_nodes.items(): + vertex_attributes[node] = { + "shape": "circle", + "style": "filled", + "fillcolor": "white", + "color": "#333333", + "label": "AND", + "width": "0.38", + "height": "0.38", + "fixedsize": "true", + "fontsize": "8", + "tooltip": _reaction_text(method.reactions[reaction_index]), + } + + edge_attributes: dict[int, dict[str, str]] = {} + + def state_attributes( + *, + interaction: int, + selected: bool, + active: bool, + raw_flow: float, + aggregate: bool, + reaction_text: str, + ) -> dict[str, str]: + nonlocal flow_scale_cursor + if not selected: + color = _UNSELECTED_NEGATIVE_COLOR if interaction < 0 else _UNSELECTED_POSITIVE_COLOR + style = "dotted" + width = 0.6 + elif condition_index is not None and not active: + color = _INACTIVE_NEGATIVE_COLOR if interaction < 0 else _INACTIVE_POSITIVE_COLOR + style = "dashed" + width = 1.0 + else: + color = _NEGATIVE_COLOR if interaction < 0 else _POSITIVE_COLOR + style = "solid" + width = 2.8 if condition_index is not None else 2.2 + if width_by == "flow": + magnitude = float(flow_scale[flow_scale_cursor]) + flow_scale_cursor += 1 + if selected: + width = 0.8 + 4.2 * magnitude + tooltip = ( + f"{reaction_text}; selected={int(selected)}" + + ("" if condition_index is None else f"; active={int(active)}") + + f"; {'aggregate ' if aggregate else ''}structural flow={raw_flow:.5g}" + ) + return { + "color": color, + "fontcolor": color, + "style": style, + "penwidth": f"{width:.3g}", + "arrowhead": "tee" if interaction < 0 else "normal", + "tooltip": tooltip, + } + + for reaction_index, reaction, selected, active in included_reactions: + reaction_text = _reaction_text(reaction) + flows = data.flow_by_reaction[reaction_index] + literals = [ + *((vertex, 1) for vertex in reaction.positive_literals), + *((vertex, -1) for vertex in reaction.negative_literals), + ] + if len(literals) == 1: + literal, interaction = literals[0] + edge_index = graph.add_edge( + literal, + reaction.product, + interaction=interaction, + reaction=reaction_index, + ) + edge_attributes[edge_index] = state_attributes( + interaction=interaction, + selected=selected, + active=active, + raw_flow=float(flows[0]), + aggregate=False, + reaction_text=reaction_text, + ) + continue + + and_node = and_nodes[reaction_index] + for literal_index, (literal, interaction) in enumerate(literals): + edge_index = graph.add_edge( + literal, + and_node, + interaction=interaction, + reaction=reaction_index, + ) + edge_attributes[edge_index] = state_attributes( + interaction=interaction, + selected=selected, + active=active, + raw_flow=float(flows[literal_index]), + aggregate=False, + reaction_text=reaction_text, + ) + aggregate_flow = float(np.sum(flows)) + edge_index = graph.add_edge( + and_node, + reaction.product, + interaction=1, + reaction=reaction_index, + ) + edge_attributes[edge_index] = state_attributes( + interaction=1, + selected=selected, + active=active, + raw_flow=aggregate_flow, + aggregate=True, + reaction_text=reaction_text, + ) + + return _ModelPlotSpec( + graph=graph, + vertex_attributes=vertex_attributes, + edge_attributes=edge_attributes, + graph_attributes={"rankdir": "LR", "pad": "0.2", "nodesep": "0.35", "ranksep": "0.5"}, + node_attributes={"fixedsize": "false", "fontname": "Helvetica", "fontsize": "10"}, + ) + + +def plot_cellnopt_model( + method: CellNOptDAG, + problem: Any = None, + *, + condition: Optional[Union[int, str]] = None, + show_unselected: bool = False, + show_inactive: bool = True, + width_by: Literal["selection", "flow"] = "selection", + renderer: str = "auto", + **plot_kwargs: Any, +) -> Any: + """Plot a solved CellNOpt model using CORNETO's graph renderers. + + ``condition=None`` shows the shared selected structure. Selecting a + condition overlays reaction activity and predicted species states. + Structural flow may optionally control edge widths, but never edge color. + """ + spec = _build_cellnopt_model_plot( + method, + problem, + condition=condition, + show_unselected=show_unselected, + show_inactive=show_inactive, + width_by=width_by, + ) + graph_attributes = dict(spec.graph_attributes) + graph_attributes.update(plot_kwargs.pop("graph_attr", {}) or {}) + node_attributes = dict(spec.node_attributes) + node_attributes.update(plot_kwargs.pop("node_attr", {}) or {}) + edge_attributes = _merge_attributes( + spec.edge_attributes, + plot_kwargs.pop("custom_edge_attr", None), + ) + vertex_attributes = _merge_attributes( + spec.vertex_attributes, + {str(vertex): attrs for vertex, attrs in (plot_kwargs.pop("custom_vertex_attr", None) or {}).items()}, + ) + return spec.graph.plot( + renderer=renderer, + graph_attr=graph_attributes, + node_attr=node_attributes, + custom_edge_attr=edge_attributes, + custom_vertex_attr=vertex_attributes, + **plot_kwargs, + ) + + +def _selected_indices( + values: Optional[Union[Any, Sequence[Any]]], + available: Sequence[Any], + *, + argument: str, +) -> list[int]: + if values is None: + return list(range(len(available))) + if isinstance(values, (str, int)) or values in available: + values = [values] + indices = [] + for value in values: + if isinstance(value, int) and argument == "conditions": + if value < 0 or value >= len(available): + raise ValueError(f"{argument} index {value} out of range.") + indices.append(value) + else: + if value not in available: + raise ValueError(f"Unknown {argument[:-1]} {value!r}; expected one of {list(available)!r}.") + indices.append(available.index(value)) + if not indices: + raise ValueError(f"{argument} must select at least one value.") + return indices + + +def _fit_selection( + data: _CellNOptPlotData, + conditions: Optional[Union[Union[int, str], Sequence[Union[int, str]]]], + signals: Optional[Union[Any, Sequence[Any]]], +) -> tuple[list[int], list[int]]: + condition_indices = _selected_indices(conditions, data.conditions, argument="conditions") + measured_vertices = [ + vertex for vertex, measured in zip(data.vertices, data.measurement_mask.any(axis=1)) if measured + ] + selected_signals = measured_vertices if signals is None else signals + signal_indices = _selected_indices(selected_signals, data.vertices, argument="signals") + return condition_indices, signal_indices + + +def _cue_data( + data: _CellNOptPlotData, + condition_indices: Sequence[int], +) -> tuple[np.ndarray, list[str], list[str]]: + selected_inputs = data.input_mask[:, condition_indices] + selected_inhibitors = data.inhibitor_mask[:, condition_indices] + stimulus_vertices = [ + index for index in range(len(data.vertices)) if np.any(selected_inputs[index] & ~selected_inhibitors[index]) + ] + inhibitor_vertices = [index for index in range(len(data.vertices)) if np.any(selected_inhibitors[index])] + labels = [str(data.vertices[index]) for index in stimulus_vertices] + labels.extend(f"{data.vertices[index]} (inh)" for index in inhibitor_vertices) + kinds = ["stimulus"] * len(stimulus_vertices) + ["inhibitor"] * len(inhibitor_vertices) + cues = np.zeros((len(condition_indices), len(labels)), dtype=float) + for row, condition_index in enumerate(condition_indices): + for column, vertex_index in enumerate(stimulus_vertices): + if not data.inhibitor_mask[vertex_index, condition_index]: + cues[row, column] = data.input_values[vertex_index, condition_index] + offset = len(stimulus_vertices) + for column, vertex_index in enumerate(inhibitor_vertices): + cues[row, offset + column] = float(data.inhibitor_mask[vertex_index, condition_index]) + return cues, labels, kinds + + +def _import_matplotlib(): + try: + import matplotlib.colors as colors + import matplotlib.pyplot as plt + from matplotlib.lines import Line2D + except ImportError as exc: + raise ImportError( + "CellNOpt fit plots require Matplotlib. Install CORNETO with `pip install 'corneto[plot]'`." + ) from exc + return plt, colors, Line2D + + +def _endpoint_fit_series( + data: _CellNOptPlotData, + condition_indices: Sequence[int], + signal_indices: Sequence[int], +) -> _FitSeries: + """Return current steady-state results with an explicit endpoint axis.""" + observed = data.measurements[np.ix_(signal_indices, condition_indices)].T + measured = data.measurement_mask[np.ix_(signal_indices, condition_indices)].T + predicted = data.states[np.ix_(signal_indices, condition_indices)].T.astype(float) + return _FitSeries( + times=np.array([0.0]), + time_labels=("Endpoint",), + observed=observed[..., np.newaxis], + measured=measured[..., np.newaxis], + predicted=predicted[..., np.newaxis], + ) + + +def _plot_fit_grid( + data: _CellNOptPlotData, + condition_indices: Sequence[int], + signal_indices: Sequence[int], + *, + figsize: Optional[tuple[float, float]], +): + plt, colors, Line2D = _import_matplotlib() + num_conditions = len(condition_indices) + num_signals = len(signal_indices) + cues, cue_labels, cue_kinds = _cue_data(data, condition_indices) + series = _endpoint_fit_series(data, condition_indices, signal_indices) + if figsize is None: + condition_label_width = min( + 4.0, + 0.09 * max(len(data.conditions[index]) for index in condition_indices), + ) + figsize = ( + max( + 6.0, + 2.15 * num_signals + max(2.6, 0.65 * len(cue_labels)) + condition_label_width, + ), + max(3.2, 1.65 * num_conditions + 0.45), + ) + width_ratios = [1.0] * num_signals + [max(1.2, 0.35 * max(len(cue_labels), 1))] + figure = plt.figure(figsize=figsize, constrained_layout=True) + layout = figure.add_gridspec(2, 1, height_ratios=[0.12, 1]) + legend_axis = figure.add_subplot(layout[0]) + legend_axis.axis("off") + panel_layout = layout[1].subgridspec( + num_conditions, + num_signals + 1, + width_ratios=width_ratios, + ) + axes = np.empty((num_conditions, num_signals + 1), dtype=object) + for row in range(num_conditions): + for column in range(num_signals + 1): + axes[row, column] = figure.add_subplot(panel_layout[row, column]) + error_cmap = colors.LinearSegmentedColormap.from_list( + "cellnopt_absolute_error", + ["#E7F5E7", "#FFF0B3", "#F4B7B2"], + ) + error_norm = colors.Normalize(vmin=0, vmax=1) + + for row, condition_index in enumerate(condition_indices): + for column, signal_index in enumerate(signal_indices): + axis = axes[row, column] + predicted = series.predicted[row, column] + observed = series.observed[row, column] + measured = series.measured[row, column] + if np.any(measured): + error = float(np.mean(np.abs(observed[measured] - predicted[measured]))) + axis.set_facecolor(error_cmap(error_norm(error))) + for time, observed_value, predicted_value, is_measured in zip( + series.times, + observed, + predicted, + measured, + ): + if is_measured: + axis.plot( + [time, time], + [observed_value, predicted_value], + color="#777777", + linewidth=1.0, + label="_nolegend_", + zorder=1, + ) + else: + axis.set_facecolor("#F2F2F2") + axis.patch.set_hatch("//") + axis.patch.set_edgecolor("#C8C8C8") + + prediction_line = "-" if len(series.times) > 1 else "none" + axis.plot( + series.times, + predicted, + color="#2C7FB8", + marker="s", + markersize=5, + linewidth=1.5, + linestyle=prediction_line, + label="Model", + zorder=2, + ) + if np.any(measured): + observed_values = np.ma.array(observed, mask=~measured) + observation_line = "-" if np.count_nonzero(measured) > 1 else "none" + axis.plot( + series.times, + observed_values, + color=_NEGATIVE_COLOR, + marker="o", + markerfacecolor="none", + markeredgewidth=1.5, + markersize=7, + linewidth=1.2, + linestyle=observation_line, + label="Observed", + zorder=3, + ) + + if len(series.times) == 1: + axis.set_xlim(series.times[0] - 0.5, series.times[0] + 0.5) + else: + span = float(series.times[-1] - series.times[0]) + margin = max(0.03 * span, 1e-6) + axis.set_xlim(series.times[0] - margin, series.times[-1] + margin) + axis.set_ylim(-0.05, 1.05) + axis.set_yticks([0, 0.5, 1]) + if row == 0: + axis.set_title(str(data.vertices[signal_index]), fontsize=10) + if row == num_conditions - 1: + axis.set_xticks(series.times, series.time_labels) + else: + axis.set_xticks([]) + if column == 0: + axis.set_ylabel( + data.conditions[condition_index], + rotation=0, + ha="right", + va="center", + labelpad=10, + ) + else: + axis.set_yticklabels([]) + axis.grid(axis="y", color="#DDDDDD", linewidth=0.5) + + cue_axis = axes[row, -1] + if cue_labels: + colors = [_STIMULUS_COLOR if kind == "stimulus" else _INHIBITOR_COLOR for kind in cue_kinds] + cue_axis.bar( + np.arange(len(cue_labels)), + cues[row], + color=colors, + edgecolor="#555555", + linewidth=0.5, + ) + cue_axis.set_xlim(-0.6, len(cue_labels) - 0.4) + cue_axis.set_ylim(0, 1.05) + cue_axis.set_yticks([]) + if row == 0: + cue_axis.set_title("Cues", fontsize=10) + if row == num_conditions - 1: + cue_axis.set_xticks( + np.arange(len(cue_labels)), + cue_labels, + rotation=45, + ha="right", + ) + else: + cue_axis.set_xticks([]) + + legend_axis.legend( + handles=[ + Line2D( + [], + [], + color=_NEGATIVE_COLOR, + marker="o", + markerfacecolor="none", + linestyle="none", + label="Observed", + ), + Line2D([], [], color="#2C7FB8", marker="s", linestyle="none", label="Model"), + ], + loc="center", + ncols=2, + frameon=False, + ) + error_scale = plt.cm.ScalarMappable(norm=error_norm, cmap=error_cmap) + error_scale.set_array([]) + colorbar = figure.colorbar( + error_scale, + ax=axes.ravel().tolist(), + fraction=0.018, + pad=0.02, + ) + colorbar.set_label("Mean absolute error") + return figure, axes + + +def _annotate_heatmap(axis: Any, values: np.ndarray, mask: Optional[np.ndarray] = None) -> None: + if values.size > 100: + return + for row in range(values.shape[0]): + for column in range(values.shape[1]): + if mask is not None and mask[row, column]: + continue + axis.text( + column, + row, + f"{values[row, column]:.2g}", + ha="center", + va="center", + fontsize=8, + ) + + +def _plot_fit_heatmaps( + data: _CellNOptPlotData, + condition_indices: Sequence[int], + signal_indices: Sequence[int], + *, + figsize: Optional[tuple[float, float]], +): + plt, colors, _ = _import_matplotlib() + observed = data.measurements[np.ix_(signal_indices, condition_indices)].T + measured = data.measurement_mask[np.ix_(signal_indices, condition_indices)].T + predicted = data.states[np.ix_(signal_indices, condition_indices)].T.astype(float) + error = np.abs(observed - predicted) + cues, cue_labels, cue_kinds = _cue_data(data, condition_indices) + condition_labels = [data.conditions[index] for index in condition_indices] + signal_labels = [str(data.vertices[index]) for index in signal_indices] + + if figsize is None: + figsize = ( + max(9.5, 1.0 * len(signal_labels) + 0.55 * len(cue_labels)), + max(3.0, 0.52 * len(condition_labels) + 1.8), + ) + figure, axes = plt.subplots( + 1, + 4, + figsize=figsize, + squeeze=False, + constrained_layout=True, + gridspec_kw={"width_ratios": [1, 1, 1, max(0.7, len(cue_labels) / max(len(signal_labels), 1))]}, + ) + axes = axes[0] + + value_cmap = plt.get_cmap("viridis").copy() + value_cmap.set_bad("#E8E8E8") + error_cmap = plt.get_cmap("RdYlGn_r").copy() + error_cmap.set_bad("#E8E8E8") + observed_masked = np.ma.array(observed, mask=~measured) + error_masked = np.ma.array(error, mask=~measured) + matrices = [observed_masked, predicted, error_masked] + titles = ["Observed", "Model", "Absolute error"] + cmaps = [value_cmap, value_cmap, error_cmap] + for axis, matrix, title, cmap in zip(axes[:3], matrices, titles, cmaps): + image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1, cmap=cmap) + axis.set_title(title) + axis.set_xticks(np.arange(len(signal_labels)), signal_labels, rotation=45, ha="right") + axis.set_yticks(np.arange(len(condition_labels)), condition_labels) + _annotate_heatmap( + axis, + np.asarray(matrix.filled(0) if np.ma.isMaskedArray(matrix) else matrix), + np.ma.getmaskarray(matrix) if np.ma.isMaskedArray(matrix) else None, + ) + figure.colorbar(image, ax=axis, fraction=0.046, pad=0.03) + + cue_axis = axes[3] + if cue_labels: + cue_codes = np.zeros_like(cues) + for column, kind in enumerate(cue_kinds): + cue_codes[:, column] = cues[:, column] * (1 if kind == "stimulus" else 2) + cue_cmap = colors.ListedColormap(["#FFFFFF", _STIMULUS_COLOR, _INHIBITOR_COLOR]) + cue_axis.imshow(cue_codes, aspect="auto", vmin=0, vmax=2, cmap=cue_cmap) + else: + cue_axis.imshow(np.zeros((len(condition_labels), 1)), aspect="auto", vmin=0, vmax=1, cmap="Greys") + cue_labels = ["None"] + cue_axis.set_title("Cues") + cue_axis.set_xticks(np.arange(len(cue_labels)), cue_labels, rotation=45, ha="right") + cue_axis.set_yticks(np.arange(len(condition_labels)), condition_labels) + return figure, axes + + +def plot_cellnopt_fit( + method: CellNOptDAG, + problem: Any = None, + *, + view: Literal["cellnopt", "heatmap"] = "cellnopt", + conditions: Optional[Union[Union[int, str], Sequence[Union[int, str]]]] = None, + signals: Optional[Union[Any, Sequence[Any]]] = None, + figsize: Optional[tuple[float, float]] = None, +): + """Compare CellNOpt measurements and predictions across conditions. + + The ``cellnopt`` view overlays observations and model predictions at the + inferred endpoint for every selected condition and signal. Unmeasured + signals show model predictions only. ``heatmap`` provides aligned matrices + for larger datasets. + """ + data = _extract_plot_data(method, problem) + condition_indices, signal_indices = _fit_selection(data, conditions, signals) + if view == "cellnopt": + return _plot_fit_grid( + data, + condition_indices, + signal_indices, + figsize=figsize, + ) + if view == "heatmap": + return _plot_fit_heatmaps( + data, + condition_indices, + signal_indices, + figsize=figsize, + ) + raise ValueError("view must be 'cellnopt' or 'heatmap'.") diff --git a/corneto/release.py b/corneto/release.py index 420f6a8dc..caddfddbc 100644 --- a/corneto/release.py +++ b/corneto/release.py @@ -55,25 +55,11 @@ def _ensure_remote_exists(remote: str) -> None: def _ensure_up_to_date_with_remote_main(remote: str) -> None: - _run(["git", "fetch", remote, "main", "dev"], check=True) + _run(["git", "fetch", remote, "main"], check=True) head = _run(["git", "rev-parse", "HEAD"], check=True) remote_main = _run(["git", "rev-parse", f"{remote}/main"], check=True) if head != remote_main: - raise ReleaseError(f"HEAD is not at {remote}/main. Pull main after merging dev -> main.") - - -def _ensure_dev_is_merged(remote: str) -> None: - # `merge-base --is-ancestor A B` succeeds when A is reachable from B. - result = subprocess.run( - ["git", "merge-base", "--is-ancestor", f"{remote}/dev", "HEAD"], - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: - raise ReleaseError( - f"{remote}/dev is not merged into current main commit yet. Merge dev -> main before releasing." - ) + raise ReleaseError(f"HEAD is not at {remote}/main. Pull main before releasing.") def _ensure_tag_does_not_exist(version: str, remote: str) -> None: @@ -103,7 +89,7 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser.add_argument( "--remote", default="origin", - help="Git remote containing the release main/dev branches (default: origin).", + help="Git remote containing the release main branch (default: origin).", ) parser.add_argument( "--yes", @@ -127,7 +113,6 @@ def main(argv: Sequence[str] | None = None) -> int: _ensure_clean_tree() _ensure_on_main() _ensure_up_to_date_with_remote_main(args.remote) - _ensure_dev_is_merged(args.remote) _ensure_tag_does_not_exist(version, args.remote) if args.dry_run: @@ -143,12 +128,6 @@ def main(argv: Sequence[str] | None = None) -> int: _create_and_push_tag(version, args.remote) print(f"Release tag pushed to {args.remote}: {version}") print("GitHub Actions will now build, publish, and create the GitHub release.") - print("Post-release sync reminder:") - print(" git checkout dev") - print(f" git pull --ff-only {args.remote} dev") - print(f" git merge --ff-only {args.remote}/main") - print(f" git push {args.remote} dev") - print("This keeps dev aligned with the latest release tag ancestry for dynamic versioning.") return 0 except ReleaseError as exc: print(f"Release aborted: {exc}", file=sys.stderr) diff --git a/docs/api/corneto.methods.rst b/docs/api/corneto.methods.rst index fad151b0d..e981e17ac 100644 --- a/docs/api/corneto.methods.rst +++ b/docs/api/corneto.methods.rst @@ -15,17 +15,33 @@ and transcription-factor mappings for one condition. Their ``build_many`` methods accept named conditions. ``milp_carnival`` remains as a compatibility formulation. +``CellNOptDAG.build`` accepts binary input, measurement, and optional inhibitor +mappings. ``build_many`` infers one shared connected reaction model while +evaluating its Boolean state independently in every named condition. + .. autosummary:: :toctree: generated/ CarnivalFlow CarnivalILP + signaling.CellNOptDAG BidirectionalPHONEMeS PHONEMeS compute_phonemes_scores milp_carnival - signaling.cellnopt_ilp.cellnoptILP + +CellNOpt visualization +~~~~~~~~~~~~~~~~~~~~~~ + +CellNOpt plotting utilities use the standard CORNETO graph renderers for +network views and return Matplotlib figure/axes objects for data-fit views. + +.. autosummary:: + :toctree: generated/ + + signaling.plot_cellnopt_model + signaling.plot_cellnopt_fit Metabolism ---------- diff --git a/docs/custom-index.html b/docs/custom-index.html index d42cb2d23..dfb0fc1b0 100644 --- a/docs/custom-index.html +++ b/docs/custom-index.html @@ -14,7 +14,7 @@ /> - + \n", - " \"Flux\n", - "
\n", - " \n", - " Figure 1: Vivek-Ananth, R. P., and Areejit Samal. \"Advances in the integration of transcriptional regulatory information into genome-scale metabolic models.\" Biosystems 147 (2016): 1-10.\n", - " \n", - "\n", - "\n", - "\n", - "\n", - "## FBA with CORNETO\n", - "\n", - "In order to define a FBA problem, you need to know the following:\n", - "\n", - "- **Genome-scale metabolic network**: This is the prior knowledge, usually known as genome-scale metabolic models (GEMs). These models represent the metabolic network through a stoichiometric matrix, where each row represents a metabolite and each column represents a reaction. The entries in the matrix are the stoichiometric coefficients of the metabolites in the reactions. These models also contain annotations such as Gene-Protein-Rules (GPRs) and default reaction bounds.\n", - "\n", - "- **Constraints**: For each reaction, there are limits or \"constraints\" on the flux based on factors like enzyme capacities. Basically, it’s setting the maximum and minimum fluxes for the reactions.\n", - "\n", - "- **Objective function**: You then define an objective to optimize, like maximizing the production of a specific metabolite or maximizing growth rate.\n", - "\n", - "- **LP Solver**: Using linear programming techniques, you can find the flux distribution that meets the constraints and optimizes the objective function.\n", - "\n", - "\n", - "CORNETO naturally supports FBA-based problems by transforming a genome scale metabolic network into a prior knowledge graph, and then modelling the FBA problem as a network flow problem. This is the main building block for creating more complex problems, such as multicondition Sparse FBA or multicondition iMAT for context-specific reconstruction.\n" + "COBRApy remains a natural tool for curating metabolic models and running established COBRA workflows. CORNETO is useful when the formulation itself must be extended or several network-inference problems must be expressed together." ] }, { "cell_type": "markdown", - "id": "83bd70ba", + "id": "concept-map", "metadata": {}, "source": [ - "## Using COBRApy\n", + "## From COBRApy to CORNETO\n", "\n", - "Here we show how can we use COBRApy to import the `textbook` metabolic network and how can we import it in CORNETO. We will also compare the FBA solution from COBRApy and CORNETO." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "662e7b90", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Set parameter Username\n", - "Set parameter LicenseID to value 2593994\n", - "Academic license - for non-commercial use only - expires 2025-12-02\n" - ] - }, - { - "data": { - "text/plain": [ - "(72, 95)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from cobra.io import load_model\n", + "| COBRApy concept | CORNETO concept |\n", + "|---|---|\n", + "| `cobra.Model` | metabolic hypergraph |\n", + "| metabolite | vertex |\n", + "| reaction and stoichiometry | directed hyperedge and its coefficients |\n", + "| reaction bounds | edge-flow bounds |\n", + "| objective coefficients | reaction objectives |\n", + "| `solution.fluxes` | `problem.expr.flow.value` |\n", + "| optimize a configured model | build and extend an optimization problem |\n", "\n", - "model = load_model(\"textbook\")\n", - "len(model.metabolites), len(model.reactions)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "86322a8f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Reaction identifierBiomass_Ecoli_core
NameBiomass Objective Function with GAM
Memory address0x165a9e840
Stoichiometry\n", - "

1.496 3pg_c + 3.7478 accoa_c + 59.81 atp_c + 0.361 e4p_c + 0.0709 f6p_c + 0.129 g3p_c + 0.205 g6p_c + 0.2557 gln__L_c + 4.9414 glu__L_c + 59.81 h2o_c + 3.547 nad_c + 13.0279 nadph_c + 1.7867 oaa_c...

\n", - "

1.496 3-Phospho-D-glycerate + 3.7478 Acetyl-CoA + 59.81 ATP + 0.361 D-Erythrose 4-phosphate + 0.0709 D-Fructose 6-phosphate + 0.129 Glyceraldehyde 3-phosphate + 0.205 D-Glucose 6-phosphate + 0.2557...

\n", - "
GPR
Lower bound0.0
Upper bound1000.0
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "biomass_rxn = model.reactions.get_by_id(\"Biomass_Ecoli_core\")\n", - "biomass_rxn" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "1f1db2c3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "solution = model.optimize()\n", - "print(solution)" + "The graph representation preserves reaction identifiers, stoichiometry, default bounds, and gene–protein–reaction annotations imported from the COBRApy model." ] }, { "cell_type": "markdown", - "id": "55cdaff4-984d-4b34-abcc-3edabd54ca44", + "id": "load-cobrapy-model", "metadata": {}, "source": [ - "## Using CORNETO" + "### Solve the COBRApy model\n", + "\n", + "We start with the familiar *E. coli* core model distributed with COBRApy." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "3b2af9de", + "execution_count": null, + "id": "imports", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - "
Installed version:v1.0.0.dev5 (latest stable: v1.0.0-alpha)
Available backends:CVXPY v1.6.4, PICOS v2.6.0
Default backend (corneto.opt):CVXPY
Installed solvers:CLARABEL, CVXOPT, GLOP, GLPK, GLPK_MI, GUROBI, HIGHS, OSQP, PDLP, PROXQP, SCIP, SCIPY, SCS
Graphviz version:v0.20.3
Installed path:/Users/pablorodriguezmier/Documents/work/repos/pablormier/corneto/corneto
Repository:https://github.com/saezlab/corneto
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "import numpy as np\n", + "from contextlib import redirect_stdout\n", + "from io import StringIO\n", "\n", - "import corneto as cn\n", + "import numpy as np\n", + "import pandas as pd\n", + "from cobra.io import load_model\n", "\n", - "cn.info()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "67301de4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(72, 95)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ "from corneto.io import cobra_model_to_graph\n", + "from corneto.methods import MultiSampleFBA\n", "\n", - "G = cobra_model_to_graph(model)\n", - "G.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d2efe710", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'__edge_type': 'directed',\n", - " 'id': 'Biomass_Ecoli_core',\n", - " '__source_attr': {'3pg_c': {'__value': -1.496},\n", - " 'accoa_c': {'__value': -3.7478},\n", - " 'atp_c': {'__value': -59.81},\n", - " 'e4p_c': {'__value': -0.361},\n", - " 'f6p_c': {'__value': -0.0709},\n", - " 'g3p_c': {'__value': -0.129},\n", - " 'g6p_c': {'__value': -0.205},\n", - " 'gln__L_c': {'__value': -0.2557},\n", - " 'glu__L_c': {'__value': -4.9414},\n", - " 'h2o_c': {'__value': -59.81},\n", - " 'nad_c': {'__value': -3.547},\n", - " 'nadph_c': {'__value': -13.0279},\n", - " 'oaa_c': {'__value': -1.7867},\n", - " 'pep_c': {'__value': -0.5191},\n", - " 'pyr_c': {'__value': -2.8328},\n", - " 'r5p_c': {'__value': -0.8977}},\n", - " '__target_attr': {'adp_c': {'__value': 59.81},\n", - " 'akg_c': {'__value': 4.1182},\n", - " 'coa_c': {'__value': 3.7478},\n", - " 'h_c': {'__value': 59.81},\n", - " 'nadh_c': {'__value': 3.547},\n", - " 'nadp_c': {'__value': 13.0279},\n", - " 'pi_c': {'__value': 59.81}},\n", - " 'default_lb': 0.0,\n", - " 'default_ub': 1000.0,\n", - " 'GPR': ''}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rid = list(G.get_edges_by_attr(\"id\", biomass_rxn.id))[0]\n", - "G.get_attr_edge(rid)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8d57a2f4", - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "coa_c\n", - "\n", - "coa_c\n", - "\n", - "\n", - "\n", - "e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "acald_c\n", - "\n", - "acald_c\n", - "\n", - "\n", - "\n", - "acald_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c\n", - "\n", - "nad_c\n", - "\n", - "\n", - "\n", - "nad_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c\n", - "\n", - "accoa_c\n", - "\n", - "\n", - "\n", - "accoa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c\n", - "\n", - "nadh_c\n", - "\n", - "\n", - "\n", - "e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c\n", - "\n", - "h_c\n", - "\n", - "\n", - "\n", - "e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "acald_e\n", - "\n", - "acald_e\n", - "\n", - "\n", - "\n", - "acald_e->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "acald_e->e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "ac_c\n", - "\n", - "ac_c\n", - "\n", - "\n", - "\n", - "e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c\n", - "\n", - "atp_c\n", - "\n", - "\n", - "\n", - "atp_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c\n", - "\n", - "adp_c\n", - "\n", - "\n", - "\n", - "e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "actp_c\n", - "\n", - "actp_c\n", - "\n", - "\n", - "\n", - "e_2_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_2_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cit_c\n", - "\n", - "cit_c\n", - "\n", - "\n", - "\n", - "e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "cit_c->e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c\n", - "\n", - "acon_C_c\n", - "\n", - "\n", - "\n", - "e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c\n", - "\n", - "h2o_c\n", - "\n", - "\n", - "\n", - "h2o_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->acon_C_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "icit_c\n", - "\n", - "icit_c\n", - "\n", - "\n", - "\n", - "e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "e_4_center->icit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h_e\n", - "\n", - "h_e\n", - "\n", - "\n", - "\n", - "e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_e\n", - "\n", - "ac_e\n", - "\n", - "\n", - "\n", - "ac_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "ac_e->e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->ac_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "amp_c\n", - "\n", - "amp_c\n", - "\n", - "\n", - "\n", - "amp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_c\n", - "\n", - "akg_c\n", - "\n", - "\n", - "\n", - "akg_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "akg_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "succoa_c\n", - "\n", - "succoa_c\n", - "\n", - "\n", - "\n", - "co2_c\n", - "\n", - "co2_c\n", - "\n", - "\n", - "\n", - "co2_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_e\n", - "\n", - "akg_e\n", - "\n", - "\n", - "\n", - "akg_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "akg_e->e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_c\n", - "\n", - "etoh_c\n", - "\n", - "\n", - "\n", - "etoh_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_c\n", - "\n", - "pi_c\n", - "\n", - "\n", - "\n", - "pi_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c\n", - "\n", - "e4p_c\n", - "\n", - "\n", - "\n", - "e4p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c\n", - "\n", - "oaa_c\n", - "\n", - "\n", - "\n", - "oaa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c\n", - "\n", - "gln__L_c\n", - "\n", - "\n", - "\n", - "gln__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c\n", - "\n", - "g6p_c\n", - "\n", - "\n", - "\n", - "f6p_c\n", - "\n", - "f6p_c\n", - "\n", - "\n", - "\n", - "g6p_c->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c\n", - "\n", - "pep_c\n", - "\n", - "\n", - "\n", - "pep_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c\n", - "\n", - "g3p_c\n", - "\n", - "\n", - "\n", - "g3p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c\n", - "\n", - "glu__L_c\n", - "\n", - "\n", - "\n", - "glu__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c\n", - "\n", - "pyr_c\n", - "\n", - "\n", - "\n", - "pyr_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c\n", - "\n", - "r5p_c\n", - "\n", - "\n", - "\n", - "r5p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "ru5p__D_c\n", - "\n", - "ru5p__D_c\n", - "\n", - "\n", - "\n", - "r5p_c->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c\n", - "\n", - "nadph_c\n", - "\n", - "\n", - "\n", - "nadph_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c\n", - "\n", - "3pg_c\n", - "\n", - "\n", - "\n", - "3pg_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c\n", - "\n", - "nadp_c\n", - "\n", - "\n", - "\n", - "nadp_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "co2_e\n", - "\n", - "co2_e\n", - "\n", - "\n", - "\n", - "co2_e->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "co2_e->e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->cit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_c\n", - "\n", - "o2_c\n", - "\n", - "\n", - "\n", - "o2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c\n", - "\n", - "q8h2_c\n", - "\n", - "\n", - "\n", - "q8h2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c\n", - "\n", - "q8_c\n", - "\n", - "\n", - "\n", - "q8_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e\n", - "\n", - "lac__D_e\n", - "\n", - "\n", - "\n", - "lac__D_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e->e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_c\n", - "\n", - "lac__D_c\n", - "\n", - "\n", - "\n", - "lac__D_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->lac__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c\n", - "\n", - "2pg_c\n", - "\n", - "\n", - "\n", - "2pg_c->3pg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c->e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e\n", - "\n", - "etoh_e\n", - "\n", - "\n", - "\n", - "etoh_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e->e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->etoh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "for_e\n", - "\n", - "for_e\n", - "\n", - "\n", - "\n", - "for_e->e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "fru_e\n", - "\n", - "fru_e\n", - "\n", - "\n", - "\n", - "fru_e->e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fru_e->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "fum_e\n", - "\n", - "fum_e\n", - "\n", - "\n", - "\n", - "fum_e->e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e\n", - "\n", - "glc__D_e\n", - "\n", - "\n", - "\n", - "glc__D_e->e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e\n", - "\n", - "gln__L_e\n", - "\n", - "\n", - "\n", - "gln__L_e->e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e\n", - "\n", - "glu__L_e\n", - "\n", - "\n", - "\n", - "glu__L_e->e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e\n", - "\n", - "h2o_e\n", - "\n", - "\n", - "\n", - "h2o_e->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e->e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e\n", - "\n", - "mal__L_e\n", - "\n", - "\n", - "\n", - "mal__L_e->e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "nh4_e\n", - "\n", - "nh4_e\n", - "\n", - "\n", - "\n", - "nh4_e->e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c\n", - "\n", - "nh4_c\n", - "\n", - "\n", - "\n", - "nh4_e->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "o2_e\n", - "\n", - "o2_e\n", - "\n", - "\n", - "\n", - "o2_e->o2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_e->e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "pi_e\n", - "\n", - "pi_e\n", - "\n", - "\n", - "\n", - "pi_e->e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e\n", - "\n", - "pyr_e\n", - "\n", - "\n", - "\n", - "pyr_e->e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "succ_e\n", - "\n", - "succ_e\n", - "\n", - "\n", - "\n", - "succ_e->e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "succ_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c\n", - "\n", - "fdp_c\n", - "\n", - "\n", - "\n", - "e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "dhap_c\n", - "\n", - "dhap_c\n", - "\n", - "\n", - "\n", - "dhap_c->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->dhap_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_c\n", - "\n", - "for_c\n", - "\n", - "\n", - "\n", - "for_c->for_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_c\n", - "\n", - "fum_c\n", - "\n", - "\n", - "\n", - "fum_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "fum_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c\n", - "\n", - "succ_c\n", - "\n", - "\n", - "\n", - "succ_c->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c\n", - "\n", - "mal__L_c\n", - "\n", - "\n", - "\n", - "mal__L_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgl_c\n", - "\n", - "6pgl_c\n", - "\n", - "\n", - "\n", - "6pgl_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->6pgl_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "13dpg_c\n", - "\n", - "13dpg_c\n", - "\n", - "\n", - "\n", - "e_48_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->g6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgc_c\n", - "\n", - "6pgc_c\n", - "\n", - "\n", - "\n", - "6pgc_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c\n", - "\n", - "xu5p__D_c\n", - "\n", - "\n", - "\n", - "ru5p__D_c->xu5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glx_c\n", - "\n", - "glx_c\n", - "\n", - "\n", - "\n", - "glx_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->glx_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->fdp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->6pgc_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->amp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->succ_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "s7p_c\n", - "\n", - "s7p_c\n", - "\n", - "\n", - "\n", - "s7p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->e4p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->s7p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G.plot(layout=\"fdp\")" + "with redirect_stdout(StringIO()):\n", + " model = load_model(\"textbook\")\n", + "biomass_id = \"Biomass_Ecoli_core\"\n", + "cobra_solution = model.optimize()\n", + "\n", + "pd.Series(\n", + " {\n", + " \"metabolites\": len(model.metabolites),\n", + " \"reactions\": len(model.reactions),\n", + " \"biomass flux\": cobra_solution.fluxes[biomass_id],\n", + " },\n", + " name=\"COBRApy model\",\n", + ")" ] }, { - "cell_type": "code", - "execution_count": 8, - "id": "1b29ccc3", + "cell_type": "markdown", + "id": "convert-and-solve", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'_flow': _flow: Variable((95,), _flow),\n", - " 'edge_has_flux': edge_has_flux: Variable((95,), edge_has_flux, boolean=True),\n", - " 'flow': _flow: Variable((95,), _flow)}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "from corneto.methods import MultiSampleFBA as FBA\n", + "### Convert the model and solve with CORNETO\n", "\n", - "d = cn.Data.from_cdict({\"fba_example\": {\"Biomass_Ecoli_core\": {\"role\": \"objective\"}}})\n", + "`cobra_model_to_graph` converts each reaction into a stoichiometric hyperedge. `MultiSampleFBA.build` then constructs the mass-balance constraints, applies the imported bounds, and adds the requested reaction objective.\n", "\n", - "m = FBA()\n", - "P = m.build(G, d)\n", - "P.expr" + "`MultiSampleFBA` minimizes a weighted objective. Therefore, the coefficient `-1` below maximizes biomass. CORNETO's general optimization API also supports an explicit `Direction.MAX` when constructing a problem directly, but `direction` is not an argument of `MultiSampleFBA.build`." ] }, { "cell_type": "code", - "execution_count": 9, - "id": "fa9bda70-6811-4fb8-86c6-18d60a7a0e2e", + "execution_count": null, + "id": "build-corneto-fba", "metadata": {}, "outputs": [], "source": [ - "P.solve(solver=\"scipy\");" + "G = cobra_model_to_graph(model)\n", + "reaction_ids = list(G.get_attr_from_edges(\"id\"))\n", + "reaction_index = {reaction_id: i for i, reaction_id in enumerate(reaction_ids)}\n", + "biomass_idx = reaction_index[biomass_id]\n", + "\n", + "problem = MultiSampleFBA().build(\n", + " G,\n", + " objectives={biomass_id: -1},\n", + ")\n", + "problem.solve(solver=\"scipy\")\n", + "\n", + "corneto_biomass = float(problem.expr.flow[biomass_idx].value)\n", + "comparison = pd.Series(\n", + " {\n", + " \"COBRApy\": cobra_solution.fluxes[biomass_id],\n", + " \"CORNETO\": corneto_biomass,\n", + " },\n", + " name=\"optimal biomass flux\",\n", + ")\n", + "\n", + "assert np.isclose(comparison[\"COBRApy\"], comparison[\"CORNETO\"], atol=1e-7)\n", + "comparison" ] }, { - "cell_type": "code", - "execution_count": 10, - "id": "9782359c-e4e5-49a7-8474-a8904abb11f9", + "cell_type": "markdown", + "id": "inspect-results", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.8739215069684303" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "opt_flux = P.expr.flow[rid].value\n", - "opt_flux" + "Fluxes follow the graph's edge order. Mapping that order back to reaction identifiers gives a labeled result analogous to `solution.fluxes`." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "54dfa3d3-6e47-4430-8d78-c41f6841f0e6", + "execution_count": null, + "id": "flux-table", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "48" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "sum(np.abs(P.expr.flow.value) >= 1e-6)" + "fluxes = pd.Series(\n", + " np.asarray(problem.expr.flow.value),\n", + " index=reaction_ids,\n", + " name=\"flux\",\n", + ")\n", + "fluxes.loc[[biomass_id, \"EX_glc__D_e\", \"EX_o2_e\", \"ATPM\"]]" ] }, { - "cell_type": "code", - "execution_count": 12, - "id": "d088eaaa-9e73-4447-ab83-c0f644bbd997", + "cell_type": "markdown", + "id": "compose-constraints", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
fluxes
ACALD-0.000000
ACALDt0.000000
ACKr0.000000
ACONTa6.007250
ACONTb6.007250
......
TALA1.496984
THD20.000000
TKT11.496984
TKT21.181498
TPI7.477382
\n", - "

95 rows × 1 columns

\n", - "
" - ], - "text/plain": [ - " fluxes\n", - "ACALD -0.000000\n", - "ACALDt 0.000000\n", - "ACKr 0.000000\n", - "ACONTa 6.007250\n", - "ACONTb 6.007250\n", - "... ...\n", - "TALA 1.496984\n", - "THD2 0.000000\n", - "TKT1 1.496984\n", - "TKT2 1.181498\n", - "TPI 7.477382\n", - "\n", - "[95 rows x 1 columns]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "import pandas as pd\n", + "## Extend the optimization problem\n", "\n", - "pd.DataFrame(P.expr.flow.value, index=G.get_attr_from_edges(\"id\"), columns=[\"fluxes\"])" + "A built CORNETO problem is still editable. For example, the following constraint limits oxygen uptake to 10 mmol gDW$^{-1}$ h$^{-1}$. In this model, exchange uptake is a negative flux, so a less-negative lower limit permits less uptake." ] }, { "cell_type": "code", - "execution_count": 13, - "id": "e0771fcb", + "execution_count": null, + "id": "oxygen-constraint", "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "coa_c\n", - "\n", - "coa_c\n", - "\n", - "\n", - "\n", - "e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "acald_c\n", - "\n", - "acald_c\n", - "\n", - "\n", - "\n", - "acald_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c\n", - "\n", - "nad_c\n", - "\n", - "\n", - "\n", - "nad_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c\n", - "\n", - "accoa_c\n", - "\n", - "\n", - "\n", - "accoa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c\n", - "\n", - "nadh_c\n", - "\n", - "\n", - "\n", - "e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c\n", - "\n", - "h_c\n", - "\n", - "\n", - "\n", - "e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "acald_e\n", - "\n", - "acald_e\n", - "\n", - "\n", - "\n", - "acald_e->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "acald_e->e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "ac_c\n", - "\n", - "ac_c\n", - "\n", - "\n", - "\n", - "e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c\n", - "\n", - "atp_c\n", - "\n", - "\n", - "\n", - "atp_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c\n", - "\n", - "adp_c\n", - "\n", - "\n", - "\n", - "e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "actp_c\n", - "\n", - "actp_c\n", - "\n", - "\n", - "\n", - "e_2_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_2_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cit_c\n", - "\n", - "cit_c\n", - "\n", - "\n", - "\n", - "e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "cit_c->e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c\n", - "\n", - "acon_C_c\n", - "\n", - "\n", - "\n", - "e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c\n", - "\n", - "h2o_c\n", - "\n", - "\n", - "\n", - "h2o_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->acon_C_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "icit_c\n", - "\n", - "icit_c\n", - "\n", - "\n", - "\n", - "e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "e_4_center->icit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h_e\n", - "\n", - "h_e\n", - "\n", - "\n", - "\n", - "e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_e\n", - "\n", - "ac_e\n", - "\n", - "\n", - "\n", - "ac_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "ac_e->e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->ac_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "amp_c\n", - "\n", - "amp_c\n", - "\n", - "\n", - "\n", - "amp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_c\n", - "\n", - "akg_c\n", - "\n", - "\n", - "\n", - "akg_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "akg_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "succoa_c\n", - "\n", - "succoa_c\n", - "\n", - "\n", - "\n", - "co2_c\n", - "\n", - "co2_c\n", - "\n", - "\n", - "\n", - "co2_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_e\n", - "\n", - "akg_e\n", - "\n", - "\n", - "\n", - "akg_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "akg_e->e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_c\n", - "\n", - "etoh_c\n", - "\n", - "\n", - "\n", - "etoh_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_c\n", - "\n", - "pi_c\n", - "\n", - "\n", - "\n", - "pi_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c\n", - "\n", - "e4p_c\n", - "\n", - "\n", - "\n", - "e4p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c\n", - "\n", - "oaa_c\n", - "\n", - "\n", - "\n", - "oaa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c\n", - "\n", - "gln__L_c\n", - "\n", - "\n", - "\n", - "gln__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c\n", - "\n", - "g6p_c\n", - "\n", - "\n", - "\n", - "f6p_c\n", - "\n", - "f6p_c\n", - "\n", - "\n", - "\n", - "g6p_c->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c\n", - "\n", - "pep_c\n", - "\n", - "\n", - "\n", - "pep_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c\n", - "\n", - "g3p_c\n", - "\n", - "\n", - "\n", - "g3p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c\n", - "\n", - "glu__L_c\n", - "\n", - "\n", - "\n", - "glu__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c\n", - "\n", - "pyr_c\n", - "\n", - "\n", - "\n", - "pyr_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c\n", - "\n", - "r5p_c\n", - "\n", - "\n", - "\n", - "r5p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "ru5p__D_c\n", - "\n", - "ru5p__D_c\n", - "\n", - "\n", - "\n", - "r5p_c->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c\n", - "\n", - "nadph_c\n", - "\n", - "\n", - "\n", - "nadph_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c\n", - "\n", - "3pg_c\n", - "\n", - "\n", - "\n", - "3pg_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c\n", - "\n", - "nadp_c\n", - "\n", - "\n", - "\n", - "nadp_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "co2_e\n", - "\n", - "co2_e\n", - "\n", - "\n", - "\n", - "co2_e->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "co2_e->e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->cit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_c\n", - "\n", - "o2_c\n", - "\n", - "\n", - "\n", - "o2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c\n", - "\n", - "q8h2_c\n", - "\n", - "\n", - "\n", - "q8h2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c\n", - "\n", - "q8_c\n", - "\n", - "\n", - "\n", - "q8_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e\n", - "\n", - "lac__D_e\n", - "\n", - "\n", - "\n", - "lac__D_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e->e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_c\n", - "\n", - "lac__D_c\n", - "\n", - "\n", - "\n", - "lac__D_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->lac__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c\n", - "\n", - "2pg_c\n", - "\n", - "\n", - "\n", - "2pg_c->3pg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c->e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e\n", - "\n", - "etoh_e\n", - "\n", - "\n", - "\n", - "etoh_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e->e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->etoh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "for_e\n", - "\n", - "for_e\n", - "\n", - "\n", - "\n", - "for_e->e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "fru_e\n", - "\n", - "fru_e\n", - "\n", - "\n", - "\n", - "fru_e->e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fru_e->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "fum_e\n", - "\n", - "fum_e\n", - "\n", - "\n", - "\n", - "fum_e->e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e\n", - "\n", - "glc__D_e\n", - "\n", - "\n", - "\n", - "glc__D_e->e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e\n", - "\n", - "gln__L_e\n", - "\n", - "\n", - "\n", - "gln__L_e->e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e\n", - "\n", - "glu__L_e\n", - "\n", - "\n", - "\n", - "glu__L_e->e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e\n", - "\n", - "h2o_e\n", - "\n", - "\n", - "\n", - "h2o_e->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e->e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e\n", - "\n", - "mal__L_e\n", - "\n", - "\n", - "\n", - "mal__L_e->e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "nh4_e\n", - "\n", - "nh4_e\n", - "\n", - "\n", - "\n", - "nh4_e->e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c\n", - "\n", - "nh4_c\n", - "\n", - "\n", - "\n", - "nh4_e->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "o2_e\n", - "\n", - "o2_e\n", - "\n", - "\n", - "\n", - "o2_e->o2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_e->e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "pi_e\n", - "\n", - "pi_e\n", - "\n", - "\n", - "\n", - "pi_e->e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e\n", - "\n", - "pyr_e\n", - "\n", - "\n", - "\n", - "pyr_e->e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "succ_e\n", - "\n", - "succ_e\n", - "\n", - "\n", - "\n", - "succ_e->e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "succ_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c\n", - "\n", - "fdp_c\n", - "\n", - "\n", - "\n", - "e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "dhap_c\n", - "\n", - "dhap_c\n", - "\n", - "\n", - "\n", - "dhap_c->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->dhap_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_c\n", - "\n", - "for_c\n", - "\n", - "\n", - "\n", - "for_c->for_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_c\n", - "\n", - "fum_c\n", - "\n", - "\n", - "\n", - "fum_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "fum_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c\n", - "\n", - "succ_c\n", - "\n", - "\n", - "\n", - "succ_c->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c\n", - "\n", - "mal__L_c\n", - "\n", - "\n", - "\n", - "mal__L_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgl_c\n", - "\n", - "6pgl_c\n", - "\n", - "\n", - "\n", - "6pgl_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->6pgl_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "13dpg_c\n", - "\n", - "13dpg_c\n", - "\n", - "\n", - "\n", - "e_48_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->g6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgc_c\n", - "\n", - "6pgc_c\n", - "\n", - "\n", - "\n", - "6pgc_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c\n", - "\n", - "xu5p__D_c\n", - "\n", - "\n", - "\n", - "ru5p__D_c->xu5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glx_c\n", - "\n", - "glx_c\n", - "\n", - "\n", - "\n", - "glx_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->glx_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->fdp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->6pgc_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->amp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->succ_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "s7p_c\n", - "\n", - "s7p_c\n", - "\n", - "\n", - "\n", - "s7p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->e4p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->s7p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "G.plot(custom_edge_attr=cn.pl.flow_style(P), layout=\"fdp\")" + "oxygen_idx = reaction_index[\"EX_o2_e\"]\n", + "\n", + "oxygen_limited = MultiSampleFBA().build(\n", + " G,\n", + " objectives={biomass_id: -1},\n", + ")\n", + "oxygen_limited += oxygen_limited.expr.flow[oxygen_idx] >= -10\n", + "oxygen_limited.solve(solver=\"scipy\")\n", + "\n", + "oxygen_limited_biomass = float(oxygen_limited.expr.flow[biomass_idx].value)\n", + "assert oxygen_limited_biomass < corneto_biomass\n", + "\n", + "pd.Series(\n", + " {\n", + " \"default medium\": corneto_biomass,\n", + " \"oxygen-limited\": oxygen_limited_biomass,\n", + " },\n", + " name=\"optimal biomass flux\",\n", + ")" ] }, { "cell_type": "markdown", - "id": "custom-media-01", + "id": "multiple-media", "metadata": {}, "source": [ - "## Passing a custom media composition\n", + "## Solve multiple media conditions\n", "\n", - "Media composition is represented as sample-specific reaction bounds. In CORNETO, each sample passed to `MultiSampleFBA` can include exchange reactions with `lower_bound` and `upper_bound` values. For exchange reactions in this model, uptake is a negative flux, so a more negative lower bound allows more uptake.\n", - "\n", - "The same data object can also include the biomass objective, which lets us solve several media conditions in one FBA problem." + "`build_many` creates one flux vector per named condition. Objectives and reaction bounds use an outer mapping with the same condition names. Conditions are solved in one problem, which also makes it possible for advanced formulations to couple them." ] }, { "cell_type": "code", "execution_count": null, - "id": "custom-media-02", + "id": "build-many", "metadata": {}, "outputs": [], "source": [ - "media = {\n", + "condition_objectives = {\n", + " \"glucose_rich\": {biomass_id: -1},\n", + " \"glucose_limited\": {biomass_id: -1},\n", + "}\n", + "condition_bounds = {\n", " \"glucose_rich\": {\n", - " \"EX_glc__D_e\": {\"lower_bound\": -10.0, \"upper_bound\": 1000.0},\n", - " \"EX_o2_e\": {\"lower_bound\": -20.0, \"upper_bound\": 1000.0},\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (-20.0, 1000.0),\n", " },\n", " \"glucose_limited\": {\n", - " \"EX_glc__D_e\": {\"lower_bound\": -2.0, \"upper_bound\": 1000.0},\n", - " \"EX_o2_e\": {\"lower_bound\": -20.0, \"upper_bound\": 1000.0},\n", + " \"EX_glc__D_e\": (-2.0, 1000.0),\n", + " \"EX_o2_e\": (-20.0, 1000.0),\n", " },\n", "}\n", "\n", - "media_data = cn.Data.from_cdict(\n", + "media_problem = MultiSampleFBA().build_many(\n", + " G,\n", + " objectives=condition_objectives,\n", + " reaction_bounds=condition_bounds,\n", + ")\n", + "media_problem.solve(solver=\"scipy\")\n", + "\n", + "condition_names = list(condition_objectives)\n", + "reported_reactions = [biomass_id, \"EX_glc__D_e\", \"EX_o2_e\"]\n", + "media_fluxes = pd.DataFrame(\n", " {\n", " condition: {\n", - " \"Biomass_Ecoli_core\": {\"role\": \"objective\"},\n", - " **bounds,\n", + " reaction_id: float(media_problem.expr.flow[reaction_index[reaction_id], column].value)\n", + " for reaction_id in reported_reactions\n", " }\n", - " for condition, bounds in media.items()\n", + " for column, condition in enumerate(condition_names)\n", " }\n", ")\n", "\n", - "media_data" + "assert media_problem.expr.flow.value.shape == (G.num_edges, len(condition_names))\n", + "assert media_fluxes.loc[biomass_id, \"glucose_limited\"] < media_fluxes.loc[biomass_id, \"glucose_rich\"]\n", + "media_fluxes" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "custom-media-03", + "cell_type": "markdown", + "id": "sparse-fba", "metadata": {}, - "outputs": [], "source": [ - "media_problem = FBA().build(G, media_data)\n", - "media_problem.solve(solver=\"scipy\");" + "## Sparse FBA\n", + "\n", + "CORNETO can associate each reaction with a binary indicator that records whether its flux is nonzero. With `lambda_reg > 0`, it penalizes the number of active reactions. Because of these indicators, sparse FBA is a mixed-integer linear program (MILP), rather than the linear program used by the standard mathematical FBA formulation.\n", + "\n", + "Here we require at least 90% of the optimal biomass and optimize biomass together with a penalty on the number of active reactions. `lambda_reg` controls that trade-off; larger values give reaction count more influence. The biomass floor prevents sparsity from being achieved by suppressing growth." ] }, { "cell_type": "code", "execution_count": null, - "id": "custom-media-04", + "id": "solve-sparse-fba", "metadata": {}, "outputs": [], "source": [ - "reaction_ids = [\"Biomass_Ecoli_core\", \"EX_glc__D_e\", \"EX_o2_e\"]\n", - "reaction_index = {rid: next(iter(G.get_edges_by_attr(\"id\", rid))) for rid in reaction_ids}\n", + "biomass_floor = 0.90 * corneto_biomass\n", + "sparse_problem = MultiSampleFBA(lambda_reg=0.1).build(\n", + " G,\n", + " objectives={biomass_id: -1},\n", + " reaction_bounds={biomass_id: (biomass_floor, None)},\n", + ")\n", + "sparse_problem.solve(solver=\"highs\")\n", + "\n", + "sparse_biomass = float(sparse_problem.expr.flow[biomass_idx].value)\n", + "standard_active = int(np.count_nonzero(np.abs(problem.expr.flow.value) > 1e-6))\n", + "sparse_active = int(np.count_nonzero(np.abs(sparse_problem.expr.flow.value) > 1e-6))\n", + "\n", + "assert sparse_biomass >= biomass_floor - 1e-7\n", + "assert sparse_active <= standard_active\n", "\n", - "pd.DataFrame(\n", + "pd.Series(\n", " {\n", - " condition: {rid: media_problem.expr.flow[reaction_index[rid], i].value for rid in reaction_ids}\n", - " for i, condition in enumerate(media_data.samples)\n", + " \"biomass floor\": biomass_floor,\n", + " \"sparse biomass\": sparse_biomass,\n", + " \"active reactions in standard solution\": standard_active,\n", + " \"active reactions in sparse solution\": sparse_active,\n", " }\n", ")" ] }, { "cell_type": "markdown", - "id": "custom-media-05", + "id": "pfba-and-next-steps", "metadata": {}, "source": [ - "The custom bounds override the default exchange bounds stored in the graph for the matching sample. Here, glucose-limited medium restricts `EX_glc__D_e` uptake and the optimal biomass flux decreases accordingly." - ] - }, - { - "cell_type": "markdown", - "id": "3f320215-867e-4882-bd9d-5dc94f629054", - "metadata": {}, - "source": [ - "## Sparse FBA" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "71523d1d-8742-493d-a07e-e56bb3821c40", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "===============================================================================\n", - " CVXPY \n", - " v1.6.4 \n", - "===============================================================================\n", - "(CVXPY) Apr 14 05:10:44 PM: Your problem has 190 variables, 453 constraints, and 1 parameters.\n", - "(CVXPY) Apr 14 05:10:44 PM: It is compliant with the following grammars: DCP, DQCP\n", - "(CVXPY) Apr 14 05:10:44 PM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.\n", - "(CVXPY) Apr 14 05:10:44 PM: Your problem is compiled with the CPP canonicalization backend.\n", - "-------------------------------------------------------------------------------\n", - " Compilation \n", - "-------------------------------------------------------------------------------\n", - "(CVXPY) Apr 14 05:10:44 PM: Compiling problem (target solver=HIGHS).\n", - "(CVXPY) Apr 14 05:10:44 PM: Reduction chain: Dcp2Cone -> CvxAttr2Constr -> ConeMatrixStuffing -> HIGHS\n", - "(CVXPY) Apr 14 05:10:44 PM: Applying reduction Dcp2Cone\n", - "(CVXPY) Apr 14 05:10:44 PM: Applying reduction CvxAttr2Constr\n", - "(CVXPY) Apr 14 05:10:44 PM: Applying reduction ConeMatrixStuffing\n", - "(CVXPY) Apr 14 05:10:44 PM: Applying reduction HIGHS\n", - "(CVXPY) Apr 14 05:10:44 PM: Finished problem compilation (took 5.011e-03 seconds).\n", - "(CVXPY) Apr 14 05:10:44 PM: (Subsequent compilations of this problem, using the same arguments, should take less time.)\n", - "-------------------------------------------------------------------------------\n", - " Numerical solver \n", - "-------------------------------------------------------------------------------\n", - "(CVXPY) Apr 14 05:10:44 PM: Invoking solver HIGHS to obtain a solution.\n", - "Running HiGHS 1.10.0 (git hash: fd86653): Copyright (c) 2025 HiGHS under MIT licence terms\n", - "MIP has 453 rows; 190 cols; 883 nonzeros; 95 integer variables (95 binary)\n", - "Coefficient ranges:\n", - " Matrix [7e-02, 1e+03]\n", - " Cost [1e-01, 1e+00]\n", - " Bound [1e+00, 1e+00]\n", - " RHS [8e-01, 1e+03]\n", - "Presolving model\n", - "153 rows, 144 cols, 493 nonzeros 0s\n", - "123 rows, 123 cols, 415 nonzeros 0s\n", - "120 rows, 119 cols, 408 nonzeros 0s\n", - "\n", - "Solving MIP model with:\n", - " 120 rows\n", - " 119 cols (78 binary, 0 integer, 0 implied int., 41 continuous)\n", - " 408 nonzeros\n", - "\n", - "Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic; L => Sub-MIP;\n", - " P => Empty MIP; R => Randomized rounding; S => Solve LP; T => Evaluate node; U => Unbounded;\n", - " z => Trivial zero; l => Trivial lower; u => Trivial upper; p => Trivial point; X => User solution\n", - "\n", - " Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work \n", - "Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time\n", - "\n", - " 0 0 0 0.00% -64.93809816 inf inf 0 0 0 0 0.0s\n", - " S 0 0 0 0.00% -64.93809816 3.685702492 1861.89% 0 0 0 0 0.0s\n", - " 0 0 0 0.00% 0.5684850774 3.685702492 84.58% 0 0 0 55 0.0s\n", - "\n", - "3.8% inactive integer columns, restarting\n", - "Model after restart has 114 rows, 116 cols (75 bin., 0 int., 0 impl., 41 cont.), and 396 nonzeros\n", - "\n", - " 0 0 0 0.00% 2.003873385 3.685702492 45.63% 40 0 0 697 0.0s\n", - " S 0 0 0 0.00% 2.003873385 3.685702492 45.63% 40 0 0 697 0.0s\n", - " R 0 0 0 0.00% 2.003921104 3.685702492 45.63% 40 39 0 748 0.1s\n", - "\n", - "Symmetry detection completed in 0.0s\n", - "Found 20 full orbitope(s) acting on 46 columns\n", - "\n", - " 64 0 32 100.00% 3.685702492 3.685702492 0.00% 2216 83 456 8185 0.3s\n", - "\n", - "Solving report\n", - " Status Optimal\n", - " Primal bound 3.68570249247\n", - " Dual bound 3.68570249247\n", - " Gap 0% (tolerance: 0.01%)\n", - " P-D integral 0.0711361691274\n", - " Solution status feasible\n", - " 3.68570249247 (objective)\n", - " 0 (bound viol.)\n", - " 0 (int. viol.)\n", - " 0 (row viol.)\n", - " Timing 0.27 (total)\n", - " 0.00 (presolve)\n", - " 0.00 (solve)\n", - " 0.00 (postsolve)\n", - " Max sub-MIP depth 3\n", - " Nodes 64\n", - " Repair LPs 0 (0 feasible; 0 iterations)\n", - " LP iterations 8185 (total)\n", - " 3720 (strong br.)\n", - " 791 (separation)\n", - " 2539 (heuristics)\n", - "-------------------------------------------------------------------------------\n", - " Summary \n", - "-------------------------------------------------------------------------------\n", - "(CVXPY) Apr 14 05:10:44 PM: Problem status: optimal\n", - "(CVXPY) Apr 14 05:10:44 PM: Optimal value: 3.686e+00\n", - "(CVXPY) Apr 14 05:10:44 PM: Compilation took 5.011e-03 seconds\n", - "(CVXPY) Apr 14 05:10:44 PM: Solver (including time spent in interface) took 2.758e-01 seconds\n" - ] - } - ], - "source": [ - "m = FBA(lambda_reg=0.1)\n", - "P = m.build(G, d)\n", - "# We add a constraint to force at least 90% of optimal flux\n", - "P += P.expr.flow[rid] >= opt_flux * 0.90\n", - "P.solve(solver=\"highs\", verbosity=1);" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "517c7e1c-b1e8-4f60-8631-46e646148315", - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "coa_c\n", - "\n", - "coa_c\n", - "\n", - "\n", - "\n", - "e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "coa_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "acald_c\n", - "\n", - "acald_c\n", - "\n", - "\n", - "\n", - "acald_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c\n", - "\n", - "nad_c\n", - "\n", - "\n", - "\n", - "nad_c->e_0_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "nad_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c\n", - "\n", - "accoa_c\n", - "\n", - "\n", - "\n", - "accoa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "accoa_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c\n", - "\n", - "nadh_c\n", - "\n", - "\n", - "\n", - "e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "nadh_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c\n", - "\n", - "h_c\n", - "\n", - "\n", - "\n", - "e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "h_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_0_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "acald_e\n", - "\n", - "acald_e\n", - "\n", - "\n", - "\n", - "acald_e->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "acald_e->e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "ac_c\n", - "\n", - "ac_c\n", - "\n", - "\n", - "\n", - "e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c\n", - "\n", - "atp_c\n", - "\n", - "\n", - "\n", - "atp_c->e_2_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "atp_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c\n", - "\n", - "adp_c\n", - "\n", - "\n", - "\n", - "e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "adp_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "actp_c\n", - "\n", - "actp_c\n", - "\n", - "\n", - "\n", - "e_2_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_2_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cit_c\n", - "\n", - "cit_c\n", - "\n", - "\n", - "\n", - "e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "cit_c->e_3_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c\n", - "\n", - "acon_C_c\n", - "\n", - "\n", - "\n", - "e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "acon_C_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c\n", - "\n", - "h2o_c\n", - "\n", - "\n", - "\n", - "h2o_c->e_4_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_10_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "h2o_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->acon_C_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_3_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "icit_c\n", - "\n", - "icit_c\n", - "\n", - "\n", - "\n", - "e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "icit_c->e_59_center\n", - "\n", - "\n", - "\n", - "\n", - "e_4_center->icit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h_e\n", - "\n", - "h_e\n", - "\n", - "\n", - "\n", - "e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_30_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "h_e->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "ac_e\n", - "\n", - "ac_e\n", - "\n", - "\n", - "\n", - "ac_e->e_5_center\n", - "\n", - "\n", - "\n", - "\n", - "e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "ac_e->e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_center->ac_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "amp_c\n", - "\n", - "amp_c\n", - "\n", - "\n", - "\n", - "amp_c->e_6_center\n", - "\n", - "\n", - "\n", - "\n", - "e_6_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_c\n", - "\n", - "akg_c\n", - "\n", - "\n", - "\n", - "akg_c->e_7_center\n", - "\n", - "\n", - "\n", - "\n", - "akg_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "succoa_c\n", - "\n", - "succoa_c\n", - "\n", - "\n", - "\n", - "co2_c\n", - "\n", - "co2_c\n", - "\n", - "\n", - "\n", - "co2_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "akg_e\n", - "\n", - "akg_e\n", - "\n", - "\n", - "\n", - "akg_e->e_8_center\n", - "\n", - "\n", - "\n", - "\n", - "e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "akg_e->e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_8_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_c\n", - "\n", - "etoh_c\n", - "\n", - "\n", - "\n", - "etoh_c->e_9_center\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->acald_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_9_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_c\n", - "\n", - "pi_c\n", - "\n", - "\n", - "\n", - "pi_c->e_11_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "pi_c->e_81_center\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_10_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_11_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c\n", - "\n", - "e4p_c\n", - "\n", - "\n", - "\n", - "e4p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e4p_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c\n", - "\n", - "oaa_c\n", - "\n", - "\n", - "\n", - "oaa_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_14_center\n", - "\n", - "\n", - "\n", - "\n", - "oaa_c->e_79_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c\n", - "\n", - "gln__L_c\n", - "\n", - "\n", - "\n", - "gln__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_53_center\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c\n", - "\n", - "g6p_c\n", - "\n", - "\n", - "\n", - "f6p_c\n", - "\n", - "f6p_c\n", - "\n", - "\n", - "\n", - "g6p_c->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "g6p_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c\n", - "\n", - "pep_c\n", - "\n", - "\n", - "\n", - "pep_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_78_center\n", - "\n", - "\n", - "\n", - "\n", - "pep_c->e_82_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c\n", - "\n", - "g3p_c\n", - "\n", - "\n", - "\n", - "g3p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_48_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "g3p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c\n", - "\n", - "glu__L_c\n", - "\n", - "\n", - "\n", - "glu__L_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "f6p_c->e_71_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c\n", - "\n", - "pyr_c\n", - "\n", - "\n", - "\n", - "pyr_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_70_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_72_center\n", - "\n", - "\n", - "\n", - "\n", - "pyr_c->e_80_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c\n", - "\n", - "r5p_c\n", - "\n", - "\n", - "\n", - "r5p_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "ru5p__D_c\n", - "\n", - "ru5p__D_c\n", - "\n", - "\n", - "\n", - "r5p_c->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "r5p_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c\n", - "\n", - "nadph_c\n", - "\n", - "\n", - "\n", - "nadph_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_54_center\n", - "\n", - "\n", - "\n", - "\n", - "nadph_c->e_67_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c\n", - "\n", - "3pg_c\n", - "\n", - "\n", - "\n", - "3pg_c->e_12_center\n", - "\n", - "\n", - "\n", - "\n", - "3pg_c->e_74_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c\n", - "\n", - "nadp_c\n", - "\n", - "\n", - "\n", - "nadp_c->e_47_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_52_center\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_58_center\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "nadp_c->e_91_center\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_12_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "co2_e\n", - "\n", - "co2_e\n", - "\n", - "\n", - "\n", - "co2_e->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "co2_e->e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_14_center->cit_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_c\n", - "\n", - "o2_c\n", - "\n", - "\n", - "\n", - "o2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c\n", - "\n", - "q8h2_c\n", - "\n", - "\n", - "\n", - "q8h2_c->e_15_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8h2_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c\n", - "\n", - "q8_c\n", - "\n", - "\n", - "\n", - "q8_c->e_66_center\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "q8_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_15_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e\n", - "\n", - "lac__D_e\n", - "\n", - "\n", - "\n", - "lac__D_e->e_16_center\n", - "\n", - "\n", - "\n", - "\n", - "e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_e->e_32_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "lac__D_c\n", - "\n", - "lac__D_c\n", - "\n", - "\n", - "\n", - "lac__D_c->e_60_center\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_16_center->lac__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c\n", - "\n", - "2pg_c\n", - "\n", - "\n", - "\n", - "2pg_c->3pg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "2pg_c->e_17_center\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e\n", - "\n", - "etoh_e\n", - "\n", - "\n", - "\n", - "etoh_e->e_18_center\n", - "\n", - "\n", - "\n", - "\n", - "e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "etoh_e->e_23_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_center->etoh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "for_e\n", - "\n", - "for_e\n", - "\n", - "\n", - "\n", - "for_e->e_24_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_e->e_41_center\n", - "\n", - "\n", - "\n", - "\n", - "e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "fru_e\n", - "\n", - "fru_e\n", - "\n", - "\n", - "\n", - "fru_e->e_25_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fru_e->e_44_center\n", - "\n", - "\n", - "\n", - "\n", - "e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "fum_e\n", - "\n", - "fum_e\n", - "\n", - "\n", - "\n", - "fum_e->e_26_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_e->e_46_center\n", - "\n", - "\n", - "\n", - "\n", - "e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e\n", - "\n", - "glc__D_e\n", - "\n", - "\n", - "\n", - "glc__D_e->e_27_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glc__D_e->e_49_center\n", - "\n", - "\n", - "\n", - "\n", - "e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e\n", - "\n", - "gln__L_e\n", - "\n", - "\n", - "\n", - "gln__L_e->e_28_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "gln__L_e->e_51_center\n", - "\n", - "\n", - "\n", - "\n", - "e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e\n", - "\n", - "glu__L_e\n", - "\n", - "\n", - "\n", - "glu__L_e->e_29_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glu__L_e->e_55_center\n", - "\n", - "\n", - "\n", - "\n", - "e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e\n", - "\n", - "h2o_e\n", - "\n", - "\n", - "\n", - "h2o_e->h2o_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "h2o_e->e_31_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e\n", - "\n", - "mal__L_e\n", - "\n", - "\n", - "\n", - "mal__L_e->e_33_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_e->e_62_center\n", - "\n", - "\n", - "\n", - "\n", - "e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "nh4_e\n", - "\n", - "nh4_e\n", - "\n", - "\n", - "\n", - "nh4_e->e_34_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c\n", - "\n", - "nh4_c\n", - "\n", - "\n", - "\n", - "nh4_e->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "o2_e\n", - "\n", - "o2_e\n", - "\n", - "\n", - "\n", - "o2_e->o2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "o2_e->e_35_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "pi_e\n", - "\n", - "pi_e\n", - "\n", - "\n", - "\n", - "pi_e->e_36_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pi_e->e_77_center\n", - "\n", - "\n", - "\n", - "\n", - "e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e\n", - "\n", - "pyr_e\n", - "\n", - "\n", - "\n", - "pyr_e->e_37_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "pyr_e->e_83_center\n", - "\n", - "\n", - "\n", - "\n", - "e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "succ_e\n", - "\n", - "succ_e\n", - "\n", - "\n", - "\n", - "succ_e->e_38_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "succ_e->e_86_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c\n", - "\n", - "fdp_c\n", - "\n", - "\n", - "\n", - "e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_39_center\n", - "\n", - "\n", - "\n", - "\n", - "fdp_c->e_40_center\n", - "\n", - "\n", - "\n", - "\n", - "dhap_c\n", - "\n", - "dhap_c\n", - "\n", - "\n", - "\n", - "dhap_c->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_39_center->dhap_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_40_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "for_c\n", - "\n", - "for_c\n", - "\n", - "\n", - "\n", - "for_c->for_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_41_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "fum_c\n", - "\n", - "fum_c\n", - "\n", - "\n", - "\n", - "fum_c->e_43_center\n", - "\n", - "\n", - "\n", - "\n", - "fum_c->e_45_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c\n", - "\n", - "succ_c\n", - "\n", - "\n", - "\n", - "succ_c->e_87_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_88_center\n", - "\n", - "\n", - "\n", - "\n", - "succ_c->e_89_center\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->q8_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_43_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_44_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c\n", - "\n", - "mal__L_c\n", - "\n", - "\n", - "\n", - "mal__L_c->e_63_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_64_center\n", - "\n", - "\n", - "\n", - "\n", - "mal__L_c->e_65_center\n", - "\n", - "\n", - "\n", - "\n", - "e_45_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_46_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgl_c\n", - "\n", - "6pgl_c\n", - "\n", - "\n", - "\n", - "6pgl_c->e_75_center\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_47_center->6pgl_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "13dpg_c\n", - "\n", - "13dpg_c\n", - "\n", - "\n", - "\n", - "e_48_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_48_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->g6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_49_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "nh4_c->e_50_center\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_50_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_51_center->gln__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_52_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_53_center->nh4_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_54_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_55_center->glu__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "6pgc_c\n", - "\n", - "6pgc_c\n", - "\n", - "\n", - "\n", - "6pgc_c->e_56_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c\n", - "\n", - "xu5p__D_c\n", - "\n", - "\n", - "\n", - "ru5p__D_c->xu5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_56_center->ru5p__D_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->akg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_58_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "glx_c\n", - "\n", - "glx_c\n", - "\n", - "\n", - "\n", - "glx_c->e_61_center\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_59_center->glx_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_60_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_61_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_62_center->mal__L_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_63_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_64_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_65_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->h_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_66_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_67_center->nadp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->nadh_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_70_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_71_center->fdp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->accoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_72_center->for_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_74_center->13dpg_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_75_center->6pgc_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_77_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_78_center->oaa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->co2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_79_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->amp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_80_center->pep_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->coa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_81_center->actp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->atp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_82_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_83_center->pyr_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_92_center\n", - "\n", - "\n", - "\n", - "\n", - "xu5p__D_c->e_93_center\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_86_center->succ_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_87_center->succ_e\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->q8h2_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_88_center->fum_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->adp_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->succoa_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_89_center->pi_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "s7p_c\n", - "\n", - "s7p_c\n", - "\n", - "\n", - "\n", - "s7p_c->e_90_center\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->e4p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_90_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nad_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->h_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_91_center->nadph_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_92_center->s7p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->g3p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_93_center->f6p_c\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G.plot(custom_edge_attr=cn.pl.flow_style(P), layout=\"fdp\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "9df7ab19-7e71-4a64-a26e-154f74a0bd5a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "45" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sum(P.expr.edge_has_flux.value > 0.5)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "155b23d2-8b66-40cf-84aa-d40c402d5012", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[objective_Biomass_Ecoli_core: Expression(AFFINE, UNKNOWN, ()),\n", - " regularization_edge_has_flux: Expression(AFFINE, NONNEGATIVE, ())]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "P.objectives" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "c6a38e48-3fae-4b00-9b60-66bde46d01d0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "45.0" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "P.objectives[1].value" + "This reaction-count criterion is not the same as parsimonious FBA (pFBA), which conventionally minimizes total flux after fixing the primary objective. Sparse FBA instead penalizes how many reactions are active.\n", + "\n", + "For several conditions, CORNETO can apply structured sparsity to the union of active reactions. A reaction shared by several conditions is then counted once, encouraging compact shared metabolic programs while retaining a separate flux vector for every condition. Continue with [Multi-condition FBA](multicondition-sfba.ipynb) for the general formulation and a worked example, followed by [gene expression integration](imat.ipynb) for context-specific metabolic inference.\n", + "\n", + "The examples above use the method-specific `build` and `build_many` interfaces. The general [`Data` interface](../method-inputs.md) remains available for workflows that require custom feature metadata or already represent measurements as CORNETO data objects." ] } ], diff --git a/docs/guide/metabolism/imat.ipynb b/docs/guide/metabolism/imat.ipynb new file mode 100644 index 000000000..29afe39dd --- /dev/null +++ b/docs/guide/metabolism/imat.ipynb @@ -0,0 +1,663 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "introduction", + "metadata": {}, + "source": [ + "# Integrating gene expression into metabolic models\n", + "\n", + "The preceding [FBA](flux-balance-analysis.ipynb) and [Multi-condition FBA](multicondition-sfba.ipynb) guides inferred feasible metabolic states from stoichiometry, reaction bounds, and required phenotypes. Gene expression provides another source of information: it can help choose which of the many feasible pathways best represents a particular biological context.\n", + "\n", + "This guide introduces gene-expression integration for one condition using **iMAT**, the method currently implemented in CORNETO. We will extract an expression-consistent metabolic state from the same COBRApy *E. coli* core model used in the FBA guides." + ] + }, + { + "cell_type": "markdown", + "id": "what-imat-adds", + "metadata": {}, + "source": [ + "## One established approach: iMAT\n", + "\n", + "[iMAT was introduced by Shlomi and colleagues](https://www.nature.com/articles/nbt.1487) to infer context-specific metabolism from expression evidence. It retains the FBA feasible space:\n", + "\n", + "$$\n", + "Sv=0, \\qquad \\ell \\leq v \\leq u.\n", + "$$\n", + "\n", + "It then uses qualitative expression evidence to select a flux state:\n", + "\n", + "- reactions supported by **high** expression are encouraged to carry flux;\n", + "- reactions associated with **low** expression are encouraged to remain inactive;\n", + "- reactions with intermediate or missing evidence are decided by network feasibility and the other objectives.\n", + "\n", + "The optimization minimizes disagreement with this evidence. It does not convert transcript abundance into a flux value, and expression cannot override mass balance, reaction bounds, or a required phenotype. Because reaction activity is represented with indicators, iMAT is a mixed-integer optimization problem.\n", + "\n", + "CORNETO provides this formulation through `MultiSampleIMAT`, but the framework is not restricted to the original iMAT objective. The built problem exposes its constraints, activity variables, and objective terms, making it straightforward to add biological constraints, compose other penalties, or implement methodological variations." + ] + }, + { + "cell_type": "markdown", + "id": "from-genes-to-reactions", + "metadata": {}, + "source": [ + "## From genes to reaction evidence\n", + "\n", + "Metabolic models associate genes with reactions through gene–protein–reaction (GPR) rules. CORNETO applies these rules before building iMAT: an `and` relationship uses the least-supported required subunit, while an `or` relationship uses the best-supported alternative enzyme.\n", + "\n", + "| Normalized expression | iMAT interpretation |\n", + "|---:|---|\n", + "| At or above the high threshold | Encourage the mapped reaction to be active |\n", + "| At or below the low threshold | Encourage the mapped reaction to be inactive |\n", + "| Between thresholds or missing | Do not score the reaction |\n", + "\n", + "`eps` defines the minimum absolute flux used to call a supported reaction active. The optional `lambda_reg` adds a small reaction-count penalty, favoring a compact network when several solutions fit the expression evidence equally well." + ] + }, + { + "cell_type": "markdown", + "id": "cobrapy-relationship", + "metadata": {}, + "source": [ + "## Relationship to COBRApy\n", + "\n", + "COBRApy remains the model-management layer in this workflow: it loads the metabolic model, exposes reactions and GPR rules, and supports standard FBA and pFBA analyses. CORNETO converts that model into its graph representation and composes the FBA constraints, expression-fit terms, reaction-activity indicators, and optional regularization in one optimization problem." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "load-model", + "metadata": {}, + "outputs": [], + "source": [ + "from contextlib import redirect_stdout\n", + "from io import StringIO\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from cobra.flux_analysis import pfba\n", + "from cobra.io import load_model\n", + "\n", + "from corneto.io import cobra_model_to_graph\n", + "from corneto.methods import MultiSampleIMAT\n", + "from corneto.methods.metabolism import evaluate_gpr_expression\n", + "\n", + "with redirect_stdout(StringIO()):\n", + " model = load_model(\"textbook\")\n", + "\n", + "model.solver = \"glpk\"\n", + "G = cobra_model_to_graph(model)\n", + "\n", + "reaction_ids = list(G.get_attr_from_edges(\"id\"))\n", + "reaction_index = {reaction_id: i for i, reaction_id in enumerate(reaction_ids)}\n", + "biomass_id = \"Biomass_Ecoli_core\"\n", + "\n", + "pd.Series(\n", + " {\n", + " \"metabolites\": len(model.metabolites),\n", + " \"reactions\": len(model.reactions),\n", + " \"genes\": len(model.genes),\n", + " \"reactions with GPR rules\": sum(bool(reaction.gene_reaction_rule) for reaction in model.reactions),\n", + " },\n", + " name=\"E. coli core model\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "example-question", + "metadata": {}, + "source": [ + "## Example: selecting an anaerobic fermentation program\n", + "\n", + "We now address a small biological question:\n", + "\n", + "> At a viable anaerobic growth rate, does the expression evidence support ethanol fermentation rather than alternative lactate or acetate routes?\n", + "\n", + "Glucose uptake is limited to 10 units and oxygen uptake is blocked. We require a biomass flux of at least `0.1`, but we do not maximize growth inside iMAT: the phenotype defines what the cell must accomplish, and expression evidence helps select how it accomplishes it.\n", + "\n", + "The expression values below are deliberately simple, synthetic normalized log-expression values. They resemble the positive scale commonly obtained after transforming expression measurements, but they do not represent a particular experiment." + ] + }, + { + "cell_type": "markdown", + "id": "growth-feasibility", + "metadata": {}, + "source": [ + "### Check that the required phenotype is feasible\n", + "\n", + "Before integrating expression, standard FBA establishes the maximum anaerobic growth supported by the medium." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "maximum-growth", + "metadata": {}, + "outputs": [], + "source": [ + "anaerobic_model = model.copy()\n", + "anaerobic_model.reactions.EX_glc__D_e.lower_bound = -10.0\n", + "anaerobic_model.reactions.EX_o2_e.bounds = (0.0, 1000.0)\n", + "\n", + "maximum_growth = anaerobic_model.optimize().fluxes[biomass_id]\n", + "minimum_growth = 0.1\n", + "\n", + "assert maximum_growth > minimum_growth\n", + "\n", + "pd.Series(\n", + " {\n", + " \"maximum anaerobic biomass\": maximum_growth,\n", + " \"minimum biomass required for inference\": minimum_growth,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "expression-profile", + "metadata": {}, + "source": [ + "### Define a qualitative expression profile\n", + "\n", + "Genes linked to ethanol production have expression values around `11–12`. Genes linked to competing lactate and acetate production have values around `4–5`. Values at or above `10` are classified as high, values at or below `6` as low, and values between the thresholds would remain unclassified." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gene-expression", + "metadata": {}, + "outputs": [], + "source": [ + "high_expression_threshold = 10.0\n", + "low_expression_threshold = 6.0\n", + "\n", + "gene_expression = {\n", + " # High expression: acetaldehyde and alcohol dehydrogenases\n", + " \"b0351\": 12.1,\n", + " \"b1241\": 11.6,\n", + " \"b0356\": 10.9,\n", + " \"b1478\": 11.4,\n", + " # Low expression: lactate dehydrogenase\n", + " \"b1380\": 4.2,\n", + " \"b2133\": 5.1,\n", + " # Low expression: phosphotransacetylase and acetate kinase\n", + " \"b2297\": 4.8,\n", + " \"b2458\": 5.3,\n", + " \"b1849\": 3.9,\n", + " \"b2296\": 4.6,\n", + " \"b3115\": 5.0,\n", + "}\n", + "\n", + "expression_modules = pd.DataFrame(\n", + " [\n", + " {\n", + " \"module\": \"ethanol formation\",\n", + " \"reactions\": \"ACALD, ALCD2x\",\n", + " \"genes\": \"b0351, b1241, b0356, b1478\",\n", + " \"expression range\": \"10.9-12.1\",\n", + " \"class\": \"high\",\n", + " },\n", + " {\n", + " \"module\": \"lactate formation\",\n", + " \"reactions\": \"LDH_D\",\n", + " \"genes\": \"b1380, b2133\",\n", + " \"expression range\": \"4.2-5.1\",\n", + " \"class\": \"low\",\n", + " },\n", + " {\n", + " \"module\": \"acetate formation\",\n", + " \"reactions\": \"PTAr, ACKr\",\n", + " \"genes\": \"b2297, b2458, b1849, b2296, b3115\",\n", + " \"expression range\": \"3.9-5.3\",\n", + " \"class\": \"low\",\n", + " },\n", + " ]\n", + ")\n", + "\n", + "expression_modules" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gpr-rules", + "metadata": {}, + "outputs": [], + "source": [ + "scored_reactions = [\"ACALD\", \"ALCD2x\", \"LDH_D\", \"PTAr\", \"ACKr\"]\n", + "gpr_table = pd.DataFrame(\n", + " {\n", + " \"reaction\": scored_reactions,\n", + " \"GPR rule\": [\n", + " model.reactions.get_by_id(reaction_id).gene_reaction_rule\n", + " for reaction_id in scored_reactions\n", + " ],\n", + " }\n", + ")\n", + "\n", + "gpr_table" + ] + }, + { + "cell_type": "markdown", + "id": "pfba-reference", + "metadata": {}, + "source": [ + "### An expression-free reference\n", + "\n", + "For comparison, COBRApy pFBA fixes biomass at the required value and minimizes total flux without using expression. This provides one compact feasible state, not a context-specific expression fit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "solve-pfba", + "metadata": {}, + "outputs": [], + "source": [ + "reference_model = model.copy()\n", + "reference_model.reactions.EX_glc__D_e.lower_bound = -10.0\n", + "reference_model.reactions.EX_o2_e.bounds = (0.0, 1000.0)\n", + "reference_model.reactions.get_by_id(biomass_id).bounds = (minimum_growth, minimum_growth)\n", + "\n", + "pfba_solution = pfba(reference_model)\n", + "\n", + "assert np.isclose(pfba_solution.fluxes[biomass_id], minimum_growth)\n", + "pfba_solution.fluxes[[biomass_id, \"EX_glc__D_e\", \"EX_lac__D_e\", \"EX_etoh_e\", \"EX_ac_e\"]].to_frame(\n", + " \"pFBA flux\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "build-imat", + "metadata": {}, + "source": [ + "### Build and solve iMAT\n", + "\n", + "`MultiSampleIMAT.build` accepts gene scores, reaction bounds, and optional reaction objectives directly. Here the biomass lower bound represents viability, so no biomass objective is needed.\n", + "\n", + "The small `lambda_reg` value breaks ties in favor of a compact active network. Expression disagreement remains the primary biological criterion in this example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "solve-imat", + "metadata": {}, + "outputs": [], + "source": [ + "eps = 1e-3\n", + "imat = MultiSampleIMAT(\n", + " eps=eps,\n", + " lambda_reg=0.01,\n", + " high_expression_threshold=high_expression_threshold,\n", + " low_expression_threshold=low_expression_threshold,\n", + ")\n", + "\n", + "imat_problem = imat.build(\n", + " G,\n", + " gene_expression=gene_expression,\n", + " reaction_bounds={\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (0.0, 1000.0),\n", + " biomass_id: (minimum_growth, None),\n", + " },\n", + ")\n", + "imat_problem.solve(solver=\"highs\")\n", + "\n", + "imat_fluxes = pd.Series(\n", + " np.asarray(imat_problem.expr.flow.value),\n", + " index=reaction_ids,\n", + " name=\"iMAT flux\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "reaction-results", + "metadata": {}, + "source": [ + "### Did the inferred state agree with expression?\n", + "\n", + "For iMAT, the main interpretation is whether a scored reaction is active, not whether its flux magnitude matches its transcript abundance. Flux signs follow the direction in which each reaction is written in the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "expression-fit", + "metadata": {}, + "outputs": [], + "source": [ + "reaction_evidence = pd.Series(\n", + " {\n", + " \"ACALD\": \"high\",\n", + " \"ALCD2x\": \"high\",\n", + " \"LDH_D\": \"low\",\n", + " \"PTAr\": \"low\",\n", + " \"ACKr\": \"low\",\n", + " },\n", + " name=\"expression evidence\",\n", + ")\n", + "\n", + "expression_fit = pd.DataFrame(\n", + " {\n", + " \"expression evidence\": reaction_evidence,\n", + " \"flux\": imat_fluxes[reaction_evidence.index],\n", + " \"active\": imat_fluxes[reaction_evidence.index].abs() >= eps * (1 - eps),\n", + " }\n", + ")\n", + "\n", + "assert expression_fit.loc[[\"ACALD\", \"ALCD2x\"], \"active\"].all()\n", + "assert not expression_fit.loc[[\"LDH_D\", \"PTAr\", \"ACKr\"], \"active\"].any()\n", + "assert imat_fluxes[biomass_id] >= minimum_growth - 1e-7\n", + "\n", + "expression_fit" + ] + }, + { + "cell_type": "markdown", + "id": "product-comparison", + "metadata": {}, + "source": [ + "### Compare the selected fermentation routes\n", + "\n", + "At the same biomass value, pFBA and iMAT answer different questions. pFBA minimizes total flux and uses both lactate and ethanol routes. iMAT instead selects a state consistent with the supplied high-ethanol and low-lactate/acetate evidence.\n", + "\n", + "The bars show representative flux solutions. Their magnitudes should not be interpreted as predictions from transcript abundance; the iMAT evidence distinguishes active from inactive reactions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "compare-products", + "metadata": {}, + "outputs": [], + "source": [ + "fermentation_products = {\n", + " \"lactate\": \"EX_lac__D_e\",\n", + " \"ethanol\": \"EX_etoh_e\",\n", + " \"acetate\": \"EX_ac_e\",\n", + "}\n", + "\n", + "product_fluxes = pd.DataFrame(\n", + " {\n", + " \"pFBA (no expression)\": {\n", + " product: pfba_solution.fluxes[reaction_id]\n", + " for product, reaction_id in fermentation_products.items()\n", + " },\n", + " \"iMAT (expression integrated)\": {\n", + " product: imat_fluxes[reaction_id]\n", + " for product, reaction_id in fermentation_products.items()\n", + " },\n", + " }\n", + ")\n", + "\n", + "assert product_fluxes.loc[\"ethanol\", \"iMAT (expression integrated)\"] > eps\n", + "assert np.isclose(product_fluxes.loc[\"lactate\", \"iMAT (expression integrated)\"], 0.0, atol=1e-7)\n", + "assert product_fluxes.loc[\"lactate\", \"pFBA (no expression)\"] > eps\n", + "\n", + "product_fluxes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot-products", + "metadata": {}, + "outputs": [], + "source": [ + "ax = product_fluxes.plot.bar(\n", + " figsize=(7, 3.5),\n", + " color=[\"#9aa0a6\", \"#2a9d8f\"],\n", + " width=0.75,\n", + ")\n", + "ax.set_ylabel(\"exchange flux\")\n", + "ax.set_xlabel(\"\")\n", + "ax.set_title(\"Fermentation products at the same biomass requirement\")\n", + "ax.tick_params(axis=\"x\", rotation=0)\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "network-plot-intro", + "metadata": {}, + "source": [ + "### Visualize the contextualized metabolic network\n", + "\n", + "The graph below contains the active iMAT network plus scored reactions that were kept inactive. High-expression active reactions are green, low-expression inactive reactions are dashed red, and active reactions without expression evidence are gray.\n", + "\n", + "| Meaning | Color and style |\n", + "|---|---|\n", + "| Active and supported by high expression | Green |\n", + "| Inactive and associated with low expression | Dashed red |\n", + "| Active without supplied expression evidence | Gray |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot-network", + "metadata": {}, + "outputs": [], + "source": [ + "active_reactions = imat_fluxes.abs() >= eps * (1 - eps)\n", + "scored_reaction_set = set(reaction_evidence.index)\n", + "displayed_reactions = np.array(\n", + " [\n", + " active_reactions.iloc[i] or reaction_id in scored_reaction_set\n", + " for i, reaction_id in enumerate(reaction_ids)\n", + " ]\n", + ")\n", + "\n", + "selected_indices = np.flatnonzero(displayed_reactions)\n", + "context_network = G.edge_subgraph(selected_indices)\n", + "edge_style = {}\n", + "\n", + "for displayed_index, original_index in enumerate(selected_indices):\n", + " reaction_id = reaction_ids[original_index]\n", + " evidence = reaction_evidence.get(reaction_id)\n", + "\n", + " if evidence == \"high\":\n", + " edge_style[displayed_index] = {\"color\": \"#2a9d8f\", \"penwidth\": \"4\"}\n", + " elif evidence == \"low\":\n", + " edge_style[displayed_index] = {\n", + " \"color\": \"#e76f51\",\n", + " \"penwidth\": \"3\",\n", + " \"style\": \"dashed\",\n", + " }\n", + " else:\n", + " edge_style[displayed_index] = {\"color\": \"#b0b0b0\", \"penwidth\": \"1.5\"}\n", + "\n", + "context_network.plot(\n", + " graph_attr={\"rankdir\": \"LR\"},\n", + " node_attr={\n", + " \"fixedsize\": \"false\",\n", + " \"shape\": \"box\",\n", + " \"style\": \"rounded\",\n", + " \"margin\": \"0.05,0.03\",\n", + " },\n", + " custom_edge_attr=edge_style,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "advanced-reaction-scores", + "metadata": {}, + "source": [ + "## Advanced: compute and supply reaction scores directly\n", + "\n", + "Passing `gene_expression=` is convenient because `MultiSampleIMAT` performs thresholding and GPR mapping automatically. For method development or custom preprocessing, it can be useful to inspect that transformation and pass the final reaction-level evidence through `reaction_scores=` instead.\n", + "\n", + "At this level, signed values no longer represent expression abundance. They encode the iMAT classification:\n", + "\n", + "| Reaction score | Meaning in the optimization |\n", + "|---:|---|\n", + "| `+1` | High-expression evidence: favor an active reaction |\n", + "| `−1` | Low-expression evidence: favor an inactive reaction |\n", + "| `0` or omitted | No expression-fit term for the reaction |\n", + "\n", + "First, threshold the positive expression measurements into categorical gene evidence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "classify-gene-expression", + "metadata": {}, + "outputs": [], + "source": [ + "gene_scores = {}\n", + "\n", + "for gene_id, expression in gene_expression.items():\n", + " if expression >= high_expression_threshold:\n", + " gene_scores[gene_id] = 1.0\n", + " elif expression <= low_expression_threshold:\n", + " gene_scores[gene_id] = -1.0\n", + " else:\n", + " gene_scores[gene_id] = 0.0\n", + "\n", + "pd.DataFrame(\n", + " {\n", + " \"expression\": pd.Series(gene_expression),\n", + " \"gene score\": pd.Series(gene_scores),\n", + " }\n", + ").sort_values(\"expression\", ascending=False)" + ] + }, + { + "cell_type": "markdown", + "id": "apply-gpr-rules", + "metadata": {}, + "source": [ + "Next, CORNETO's `evaluate_gpr_expression` applies every model GPR rule to the gene scores in one call. Reactions with a final score of zero are omitted, so they remain governed by stoichiometry, bounds, and regularization rather than expression-fit terms. For several samples, the related `evaluate_gpr_rules` function evaluates the same rule list against multiple gene-score mappings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "compute-reaction-scores", + "metadata": {}, + "outputs": [], + "source": [ + "gpr_rules = list(G.get_attr_from_edges(\"GPR\"))\n", + "reaction_score_values = evaluate_gpr_expression(gpr_rules, gene_scores)\n", + "\n", + "reaction_scores = {\n", + " reaction_id: float(score)\n", + " for reaction_id, score in zip(reaction_ids, reaction_score_values)\n", + " if not np.isclose(score, 0.0)\n", + "}\n", + "\n", + "reaction_score_table = pd.DataFrame(\n", + " {\n", + " \"GPR rule\": {\n", + " reaction_id: gpr_rules[reaction_index[reaction_id]]\n", + " for reaction_id in reaction_scores\n", + " },\n", + " \"reaction score\": reaction_scores,\n", + " }\n", + ")\n", + "\n", + "reaction_score_table" + ] + }, + { + "cell_type": "markdown", + "id": "solve-from-reaction-scores", + "metadata": {}, + "source": [ + "The reaction-score interface is closer to the optimization problem and is useful when reaction evidence comes from another mapping method or has already been curated. Threshold arguments are unnecessary because the scores already encode high and low reaction evidence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "reaction-score-problem", + "metadata": {}, + "outputs": [], + "source": [ + "reaction_score_problem = MultiSampleIMAT(\n", + " eps=eps,\n", + " lambda_reg=0.01,\n", + ").build(\n", + " G,\n", + " reaction_scores=reaction_scores,\n", + " reaction_bounds={\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (0.0, 1000.0),\n", + " biomass_id: (minimum_growth, None),\n", + " },\n", + ")\n", + "reaction_score_problem.solve(solver=\"highs\")\n", + "\n", + "reaction_score_fluxes = pd.Series(\n", + " np.asarray(reaction_score_problem.expr.flow.value),\n", + " index=reaction_ids,\n", + ")\n", + "\n", + "interface_comparison = pd.DataFrame(\n", + " {\n", + " \"reaction score\": pd.Series(reaction_scores),\n", + " \"active from gene_expression\": (\n", + " imat_fluxes[list(reaction_scores)].abs() >= eps * (1 - eps)\n", + " ),\n", + " \"active from reaction_scores\": (\n", + " reaction_score_fluxes[list(reaction_scores)].abs() >= eps * (1 - eps)\n", + " ),\n", + " }\n", + ")\n", + "\n", + "assert (\n", + " interface_comparison[\"active from gene_expression\"]\n", + " == interface_comparison[\"active from reaction_scores\"]\n", + ").all()\n", + "\n", + "interface_comparison" + ] + }, + { + "cell_type": "markdown", + "id": "interpretation-limitations", + "metadata": {}, + "source": [ + "## Interpretation and limitations\n", + "\n", + "The inferred state is stoichiometrically feasible, satisfies the anaerobic medium and growth requirement, activates the expression-supported ethanol route, and avoids the low-expression lactate and acetate routes. This illustrates the role of iMAT: expression resolves part of the ambiguity left by FBA by ranking feasible reaction-activity patterns.\n", + "\n", + "The result remains a modeling hypothesis:\n", + "\n", + "- expression is a cue for likely activity, not an activity or flux measurement; protein abundance, post-translational modification, and metabolite-level regulation can decouple expression from flux;\n", + "- thresholds determine which genes contribute evidence and should be checked for sensitivity;\n", + "- iMAT infers reaction activity, not flux magnitude from expression;\n", + "- low-expression reactions may remain active when network constraints or the required phenotype need them;\n", + "- alternative expression-consistent optima may still exist.\n", + "\n", + "The [original iMAT paper](https://www.nature.com/articles/nbt.1487) reports a central role for post-transcriptional regulation and uses disagreements between expression and predicted activity to generate hypotheses about regulation beyond transcript abundance. They do not identify a mechanism on their own: additional protein, enzyme-activity, metabolite, or flux measurements are needed to distinguish biological regulation from data and model assumptions.\n", + "\n", + "Real analyses should inspect threshold choices, biological constraints, expression coverage, and alternative solutions. Continue with [multi-condition gene expression integration](multicondition-imat.ipynb) to learn how CORNETO couples expression-informed metabolic inference across several contexts." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/guide/metabolism/index.md b/docs/guide/metabolism/index.md index f4a2bd44d..dba7ae4da 100644 --- a/docs/guide/metabolism/index.md +++ b/docs/guide/metabolism/index.md @@ -1,11 +1,20 @@ # Metabolism -CORNETO supports flux balance analysis workflows while adding modeling flexibility to define custom constraints, objectives, and coupled formulations in a single optimization problem. For users familiar with COBRA-style modeling, this enables extension from standard FBA to explicit use cases such as multi-condition FBA and integration of omics data into metabolic networks to detect stoichiometrically consistent steady-state metabolic states. +CORNETO brings constraint-based metabolic modeling into the same optimization +framework used for other network-inference problems. It interoperates with +COBRApy models and supports standard flux balance analysis while exposing the +formulation for custom constraints, coupled conditions, shared sparsity, and +omics integration. + +Start with standard FBA and model interoperability, continue with +multi-condition FBA and shared reaction selection, and then integrate +expression evidence first in one condition and later across several conditions. ```{toctree} :maxdepth: 3 flux-balance-analysis.ipynb multicondition-sfba.ipynb +imat.ipynb multicondition-imat.ipynb ``` diff --git a/docs/guide/metabolism/multicondition-imat.ipynb b/docs/guide/metabolism/multicondition-imat.ipynb index 706a3214b..ae412eabe 100644 --- a/docs/guide/metabolism/multicondition-imat.ipynb +++ b/docs/guide/metabolism/multicondition-imat.ipynb @@ -2,1586 +2,617 @@ "cells": [ { "cell_type": "markdown", - "id": "5138cc3a", + "id": "introduction", "metadata": {}, "source": [ - "# Multi-condition iMAT\n", + "# Multi-condition gene expression integration\n", "\n", - "This tutorial shows how to use the multi-condition iMAT method.\n", + "The [single-condition expression guide](imat.ipynb) showed how iMAT converts expression evidence into reaction-level preferences, while the [Multi-condition FBA guide](multicondition-sfba.ipynb) explained why reaction selection should be performed jointly when shared and context-specific metabolism are the biological quantities of interest.\n", "\n", - "We use a branched network with weak and conflicting evidence across two\n", - "conditions to illustrate how structured regularization can align solutions\n", - "across samples.\n" + "This guide combines those ideas without repeating their derivations. Each condition receives its own reaction scores, medium bounds, phenotype constraints, and flux vector. CORNETO then fits the condition-specific expression evidence while selecting one compact reaction union across the complete experiment." + ] + }, + { + "cell_type": "markdown", + "id": "what-changes", + "metadata": {}, + "source": [ + "## What changes in the multi-condition formulation?\n", + "\n", + "`MultiSampleIMAT.build_many` accepts mappings whose first level contains condition names:\n", + "\n", + "| Input | Condition-specific information |\n", + "|---|---|\n", + "| `reaction_scores` | Reactions favored active (`+1`), favored inactive (`−1`), or unscored |\n", + "| `reaction_bounds` | Medium, perturbations, and required phenotypes |\n", + "| `objectives` | Optional reaction objectives |\n", + "\n", + "Every condition keeps a separate flux vector and a separate expression-fit objective. With `lambda_reg > 0`, a reaction active in one or several conditions contributes once to the union penalty. Reuse is therefore encouraged, but conflicting or condition-specific expression evidence can still select different pathways.\n", + "\n", + "This is the same union-level coupling introduced in Multi-condition FBA, now applied while every condition also fits its own expression evidence. The joint model does not average expression profiles and does not force equal fluxes. Reaction-level agreement must therefore be inspected alongside the inferred network: an unmatched score can reveal tension between expression, network feasibility, phenotype constraints, and shared regularization." ] }, { "cell_type": "code", - "execution_count": 1, - "id": "e7dd7a9f", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:35.458243Z", - "iopub.status.busy": "2026-02-15T11:15:35.458079Z", - "iopub.status.idle": "2026-02-15T11:15:36.890934Z", - "shell.execute_reply": "2026-02-15T11:15:36.890647Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - "
Installed version:v1.0.0b3.post1.dev0+048af53c
Available backends:CVXPY v1.8.1, PICOS v2.6.2
Default backend (corneto.opt):CVXPY
Installed solvers:CVXOPT, GLPK, GLPK_MI, HIGHS, SCIP, SCIPY
Plot backend (default):auto -> graphviz
Available plot backends:graphviz v0.20.3; networkx v3.6+mpl v3.10.8; graphviz-wasm
Installed path:/Users/pablorodriguezmier/Documents/work/projects/corneto/corneto
Repository:https://github.com/saezlab/corneto
\n", - "
\n", - " " - ], - "text/plain": [ - "Installed version: v1.0.0b3.post1.dev0+048af53c\n", - "Available backends: CVXPY v1.8.1, PICOS v2.6.2\n", - "Default backend (corneto.opt): CVXPY\n", - "Installed solvers: CVXOPT, GLPK, GLPK_MI, HIGHS, SCIP, SCIPY\n", - "Plot backend (default): auto -> graphviz\n", - "Available plot backends: graphviz v0.20.3; networkx v3.6+mpl v3.10.8; graphviz-wasm\n", - "Installed path: /Users/pablorodriguezmier/Documents/work/projects/corneto/corneto\n", - "Repository: https://github.com/saezlab/corneto" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "load-model", + "metadata": {}, + "outputs": [], "source": [ + "from contextlib import redirect_stdout\n", + "from io import StringIO\n", + "\n", "import numpy as np\n", "import pandas as pd\n", + "from cobra.io import load_model\n", "\n", - "import corneto as cn\n", - "from corneto.methods.imat import MultiSampleIMAT\n", + "from corneto.io import cobra_model_to_graph\n", + "from corneto.methods import MultiSampleIMAT\n", "\n", - "cn.info()" + "with redirect_stdout(StringIO()):\n", + " model = load_model(\"textbook\")\n", + "\n", + "G = cobra_model_to_graph(model)\n", + "reaction_ids = list(G.get_attr_from_edges(\"id\"))\n", + "reaction_index = {reaction_id: i for i, reaction_id in enumerate(reaction_ids)}\n", + "biomass_id = \"Biomass_Ecoli_core\"" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "b4fabc4d", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.892236Z", - "iopub.status.busy": "2026-02-15T11:15:36.892070Z", - "iopub.status.idle": "2026-02-15T11:15:36.898171Z", - "shell.execute_reply": "2026-02-15T11:15:36.897863Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
edge_indexreaction_id
00EX_A
11R_AB
22R_BC
33R_AD
44R_DC
55R_CE
66R_EF
77BIOMASS
88R_BX
99R_X_sink
1010R_DY
1111R_Y_sink
\n", - "
" - ], - "text/plain": [ - " edge_index reaction_id\n", - "0 0 EX_A\n", - "1 1 R_AB\n", - "2 2 R_BC\n", - "3 3 R_AD\n", - "4 4 R_DC\n", - "5 5 R_CE\n", - "6 6 R_EF\n", - "7 7 BIOMASS\n", - "8 8 R_BX\n", - "9 9 R_X_sink\n", - "10 10 R_DY\n", - "11 11 R_Y_sink" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "cell_type": "markdown", + "id": "biological-question", + "metadata": {}, "source": [ - "G = cn.Graph()\n", + "## Example: aerobic respiration and anaerobic fermentation\n", "\n", - "# Uptake\n", - "G.add_edge((), \"A\", id=\"EX_A\", default_lb=0, default_ub=10)\n", + "We ask:\n", "\n", - "# Branch 1\n", - "G.add_edge(\"A\", \"B\", id=\"R_AB\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"B\", \"C\", id=\"R_BC\", default_lb=0, default_ub=100)\n", + "> Which reactions form a shared expression-consistent program for growth on glucose, and which distinguish aerobic respiration from anaerobic ethanol fermentation?\n", "\n", - "# Branch 2\n", - "G.add_edge(\"A\", \"D\", id=\"R_AD\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"D\", \"C\", id=\"R_DC\", default_lb=0, default_ub=100)\n", + "The example uses two synthetic expression contexts in the *E. coli* core model. Both grow on glucose. Oxygen is available only in the aerobic condition, and biomass is fixed at approximately 60% of the maximum supported by each environment. The fixed phenotypes keep the independent and joint comparisons biologically equivalent.\n", "\n", - "# Shared pathway to objective\n", - "G.add_edge(\"C\", \"E\", id=\"R_CE\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"E\", \"F\", id=\"R_EF\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"F\", (), id=\"BIOMASS\", default_lb=0, default_ub=100)\n", + "We start from reaction scores, the advanced interface introduced at the end of the single-condition guide. In a real workflow these scores could be produced from expression measurements and GPR rules with `evaluate_gpr_expression` or supplied by another curated mapping procedure." + ] + }, + { + "cell_type": "markdown", + "id": "define-evidence", + "metadata": {}, + "source": [ + "### Define condition-specific reaction evidence\n", "\n", - "# Side drains\n", - "G.add_edge(\"B\", \"X\", id=\"R_BX\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"X\", (), id=\"R_X_sink\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"D\", \"Y\", id=\"R_DY\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"Y\", (), id=\"R_Y_sink\", default_lb=0, default_ub=100)\n", + "The aerobic profile supports respiration and pyruvate oxidation while arguing against fermentation. The anaerobic profile supports ethanol formation while arguing against oxygen-dependent respiration and lactate production.\n", "\n", - "pd.DataFrame(\n", - " {\n", - " \"edge_index\": range(G.num_edges),\n", - " \"reaction_id\": [G.get_attr_edge(i).get(\"id\") for i in range(G.num_edges)],\n", - " }\n", - ")" + "To make model–data disagreement visible, the synthetic anaerobic profile deliberately classifies `PFL` as low even though pyruvate-formate lyase provides the route from pyruvate to acetyl-CoA and formate in the selected fermentative state. This controlled conflict lets us distinguish a reaction that is fitted from one that remains active despite low-expression evidence." ] }, { "cell_type": "code", - "execution_count": 3, - "id": "78790088", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.899476Z", - "iopub.status.busy": "2026-02-15T11:15:36.899391Z", - "iopub.status.idle": "2026-02-15T11:15:36.913618Z", - "shell.execute_reply": "2026-02-15T11:15:36.913277Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "digraph {\n", - "\tnode [fixedsize=true]\n", - "\te_0_source [shape=point]\n", - "\tA [shape=circle]\n", - "\tA [shape=circle]\n", - "\tB [shape=circle]\n", - "\tB [shape=circle]\n", - "\tC [shape=circle]\n", - "\tA [shape=circle]\n", - "\tD [shape=circle]\n", - "\tD [shape=circle]\n", - "\tC [shape=circle]\n", - "\tC [shape=circle]\n", - "\tE [shape=circle]\n", - "\tE [shape=circle]\n", - "\tF [shape=circle]\n", - "\te_7_target [shape=point]\n", - "\tF [shape=circle]\n", - "\tB [shape=circle]\n", - "\tX [shape=circle]\n", - "\te_9_target [shape=point]\n", - "\tX [shape=circle]\n", - "\tD [shape=circle]\n", - "\tY [shape=circle]\n", - "\te_11_target [shape=point]\n", - "\tY [shape=circle]\n", - "\te_0_source -> A [arrowhead=normal]\n", - "\tA -> B [arrowhead=normal]\n", - "\tB -> C [arrowhead=normal]\n", - "\tA -> D [arrowhead=normal]\n", - "\tD -> C [arrowhead=normal]\n", - "\tC -> E [arrowhead=normal]\n", - "\tE -> F [arrowhead=normal]\n", - "\tF -> e_7_target [arrowhead=normal]\n", - "\tB -> X [arrowhead=normal]\n", - "\tX -> e_9_target [arrowhead=normal]\n", - "\tD -> Y [arrowhead=normal]\n", - "\tY -> e_11_target [arrowhead=normal]\n", - "}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "reaction-scores", + "metadata": {}, + "outputs": [], + "source": [ + "condition_names = [\"glucose aerobic\", \"glucose anaerobic\"]\n", + "\n", + "reaction_scores = {\n", + " \"glucose aerobic\": {\n", + " \"CYTBD\": 1.0,\n", + " \"NADH16\": 1.0,\n", + " \"PDH\": 1.0,\n", + " \"CS\": 1.0,\n", + " \"ATPS4r\": 1.0,\n", + " \"PFL\": -1.0,\n", + " \"LDH_D\": -1.0,\n", + " \"ACALD\": -1.0,\n", + " \"ALCD2x\": -1.0,\n", + " },\n", + " \"glucose anaerobic\": {\n", + " \"PFL\": -1.0,\n", + " \"ACALD\": 1.0,\n", + " \"ALCD2x\": 1.0,\n", + " \"CYTBD\": -1.0,\n", + " \"NADH16\": -1.0,\n", + " \"PDH\": -1.0,\n", + " \"LDH_D\": -1.0,\n", + " },\n", + "}\n", + "\n", + "score_table = pd.DataFrame(reaction_scores).fillna(0.0)\n", + "score_table.index.name = \"reaction\"\n", + "score_table" + ] + }, + { + "cell_type": "markdown", + "id": "define-phenotypes", + "metadata": {}, "source": [ - "G.plot()" + "### Define the media and required phenotypes\n", + "\n", + "A negative exchange lower bound allows uptake. Biomass is fixed rather than maximized because expression agreement and shared reaction selection are the objectives of this analysis." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "582902c0", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.915144Z", - "iopub.status.busy": "2026-02-15T11:15:36.915031Z", - "iopub.status.idle": "2026-02-15T11:15:36.917329Z", - "shell.execute_reply": "2026-02-15T11:15:36.917081Z" - } - }, + "execution_count": null, + "id": "reaction-bounds", + "metadata": {}, "outputs": [], "source": [ - "w = 0.2\n", - "\n", - "scores_condition_1 = {\n", - " \"R_AB\": +w,\n", - " \"R_BC\": +w,\n", - " \"R_AD\": -w,\n", - " \"R_DC\": -w,\n", - " \"R_BX\": -0.1,\n", - " \"R_DY\": -0.1,\n", + "biomass_targets = {\n", + " \"glucose aerobic\": 0.50,\n", + " \"glucose anaerobic\": 0.12,\n", "}\n", "\n", - "scores_condition_2 = {\n", - " \"R_AB\": -w,\n", - " \"R_BC\": -w,\n", - " \"R_AD\": +w,\n", - " \"R_DC\": +w,\n", - " \"R_BX\": -0.1,\n", - " \"R_DY\": -0.1,\n", + "reaction_bounds = {\n", + " \"glucose aerobic\": {\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (-20.0, 1000.0),\n", + " biomass_id: (biomass_targets[\"glucose aerobic\"], biomass_targets[\"glucose aerobic\"]),\n", + " },\n", + " \"glucose anaerobic\": {\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (0.0, 1000.0),\n", + " biomass_id: (biomass_targets[\"glucose anaerobic\"], biomass_targets[\"glucose anaerobic\"]),\n", + " },\n", "}\n", "\n", + "pd.DataFrame(\n", + " {\n", + " \"glucose lower bound\": {condition: bounds[\"EX_glc__D_e\"][0] for condition, bounds in reaction_bounds.items()},\n", + " \"oxygen lower bound\": {condition: bounds[\"EX_o2_e\"][0] for condition, bounds in reaction_bounds.items()},\n", + " \"fixed biomass\": biomass_targets,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "scale-condition-scores", + "metadata": {}, + "source": [ + "## Balance expression evidence across conditions\n", + "\n", + "Before coupling conditions, compare their total evidence weight. Without scaling, a condition with more scored reactions or larger score magnitudes contributes more to the joint objective. The shared sparsity penalty can then overwhelm a weakly weighted condition, especially when zero flux is feasible.\n", + "\n", + "`MultiSampleIMAT(scale=True)` applies L1 normalization independently within each condition:\n", + "\n", + "$$\n", + "\\widetilde{w}_{r,c} = 100\\,\\frac{w_{r,c}}{\\sum_j |w_{j,c}|}.\n", + "$$\n", + "\n", + "The signs and relative weights within a condition are preserved, while the absolute weights sum to `100`. Each condition therefore has the same maximum expression-mismatch budget.\n", + "\n", + "The value `100` is not mathematically special: normalizing to `1` and dividing `lambda_reg` by `100` would define the same trade-off. A budget of `100` usually keeps individual objective coefficients closer to order one when tens or hundreds of reactions are scored, avoiding unnecessarily tiny coefficients in the MILP.\n", + "\n", + "Scaling and phenotype constraints solve different problems:\n", + "\n", + "- **scaling** balances the influence of expression evidence across conditions;\n", + "- **phenotype constraints** prevent biologically required conditions from collapsing to a zero or otherwise trivial flux state.\n", "\n", - "def sample_from_scores(scores):\n", - " sample = {\"BIOMASS\": {\"role\": \"objective\"}}\n", - " for rid, value in scores.items():\n", - " sample[rid] = {\"mapping\": \"edge\", \"value\": float(value)}\n", - " return sample\n", + "Use scaling when score ranges or expression coverage differ for technical reasons. Leave scores unscaled when their absolute magnitudes are already comparable and intentionally encode confidence across conditions.\n", "\n", + "After scaling, compare the objective scales explicitly. For $R$ model reactions, the expression-mismatch term is bounded by `100` per condition, while the union penalty is bounded by $\\lambda R$. If $\\lambda R$ approaches `100`, sparsity can compete with an entire condition's evidence budget. Collapse can occur earlier because an all-zero condition mismatches only its positive evidence; low-expression evidence already favors zero flux.\n", "\n", - "multi_data = cn.Data.from_cdict(\n", + "Objective scaling does not correct loose big-M constraints. iMAT activity indicators depend on reaction flux bounds, so finite, biologically meaningful bounds are also important for MILP numerical stability and performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "score-weight-comparison", + "metadata": {}, + "outputs": [], + "source": [ + "eps = 1e-3\n", + "lambda_reg = 0.01\n", + "activity_tolerance = eps * (1 - eps)\n", + "\n", + "score_weight_comparison = pd.DataFrame(\n", " {\n", - " \"condition_1\": sample_from_scores(scores_condition_1),\n", - " \"condition_2\": sample_from_scores(scores_condition_2),\n", + " \"unscaled absolute score sum\": score_table.abs().sum(),\n", + " \"absolute score sum with scale=True\": 100.0,\n", " }\n", ")\n", + "score_weight_comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "objective-scale", + "metadata": {}, + "outputs": [], + "source": [ + "pd.Series(\n", + " {\n", + " \"expression-mismatch budget per condition\": 100.0,\n", + " \"regularization cost per union reaction\": lambda_reg,\n", + " \"maximum union penalty in this model\": lambda_reg * len(reaction_ids),\n", + " },\n", + " name=\"objective scale\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "independent-reference", + "metadata": {}, + "source": [ + "## Independent iMAT as a reference\n", "\n", - "single_data_c1 = cn.Data.from_cdict({\"condition_1\": sample_from_scores(scores_condition_1)})\n", - "single_data_c2 = cn.Data.from_cdict({\"condition_2\": sample_from_scores(scores_condition_2)})" + "We first solve each condition separately with the same small reaction-count penalty used below. These runs fit each expression profile independently and provide the post-hoc union against which the joint analysis will be compared." ] }, { "cell_type": "code", - "execution_count": 5, - "id": "40470e3b", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.918357Z", - "iopub.status.busy": "2026-02-15T11:15:36.918264Z", - "iopub.status.idle": "2026-02-15T11:15:36.984038Z", - "shell.execute_reply": "2026-02-15T11:15:36.983730Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "Problem(Minimize(Expression(AFFINE, UNKNOWN, ())), [Inequality(Constant(CONSTANT, ZERO, (12,))), Inequality(Variable((12,), _flow)), Equality(Expression(AFFINE, UNKNOWN, (8,)), Constant(CONSTANT, ZERO, ())), Inequality(Expression(AFFINE, ZERO, (12,))), Inequality(Variable((12,), _flow)), Inequality(Expression(AFFINE, NONNEGATIVE, (6,))), Equality(Expression(AFFINE, NONNEGATIVE, (6,)), Constant(CONSTANT, ZERO, ())), Inequality(Expression(AFFINE, NONNEGATIVE, (6,))), Inequality(Expression(AFFINE, UNKNOWN, (6,))), Inequality(Expression(AFFINE, NONNEGATIVE, (6,)))])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "solve-independent", + "metadata": {}, + "outputs": [], "source": [ - "imat_multi_uncoupled = MultiSampleIMAT(lambda_reg=0.0, eps=1e-3)\n", - "P_multi_uncoupled = imat_multi_uncoupled.build(G, multi_data)\n", - "P_multi_uncoupled.solve(solver=\"highs\")\n", + "independent_fluxes = {}\n", "\n", - "imat_multi_coupled = MultiSampleIMAT(lambda_reg=0.5, eps=1e-3)\n", - "P_multi_coupled = imat_multi_coupled.build(G, multi_data)\n", - "P_multi_coupled.solve(solver=\"highs\")\n", + "for condition in condition_names:\n", + " problem = MultiSampleIMAT(eps=eps, lambda_reg=lambda_reg, scale=True).build(\n", + " G,\n", + " reaction_scores=reaction_scores[condition],\n", + " reaction_bounds=reaction_bounds[condition],\n", + " )\n", + " problem.solve(solver=\"highs\")\n", + " independent_fluxes[condition] = np.asarray(problem.expr.flow.value)\n", "\n", - "imat_single_c1 = MultiSampleIMAT(lambda_reg=0.5, eps=1e-3)\n", - "P_single_c1 = imat_single_c1.build(G, single_data_c1)\n", - "P_single_c1.solve(solver=\"highs\")\n", + "independent_fluxes = pd.DataFrame(independent_fluxes, index=reaction_ids)\n", + "independent_activity = independent_fluxes.abs() > activity_tolerance\n", "\n", - "imat_single_c2 = MultiSampleIMAT(lambda_reg=0.5, eps=1e-3)\n", - "P_single_c2 = imat_single_c2.build(G, single_data_c2)\n", - "P_single_c2.solve(solver=\"highs\")" + "pd.DataFrame(\n", + " {\n", + " \"biomass\": independent_fluxes.loc[biomass_id],\n", + " \"active reactions\": independent_activity.sum(),\n", + " }\n", + ")" ] }, { "cell_type": "markdown", - "id": "103264dc", + "id": "joint-analysis", "metadata": {}, "source": [ - "## Objective values\n", + "## Solve both expression contexts jointly\n", "\n", - "`P.objectives` provides the solved objective terms and values directly.\n" + "The scientific inputs have the same readable structure as the independent calls. `build_many` creates one flux column per named condition, and `lambda_reg` now penalizes their reaction union." ] }, { "cell_type": "code", - "execution_count": 6, - "id": "ad7bdf16", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.985288Z", - "iopub.status.busy": "2026-02-15T11:15:36.985211Z", - "iopub.status.idle": "2026-02-15T11:15:36.991998Z", - "shell.execute_reply": "2026-02-15T11:15:36.991688Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
settingterm_indexterm_namevalueweightweighted_value
0multi lambda=00objective_condition_1__BIOMASS-10.01.0-10.0
1multi lambda=01objective_condition_2__BIOMASS-10.01.0-10.0
2multi lambda=02imat_fit_pos_condition_1_00.01.00.0
3multi lambda=03imat_fit_neg_condition_1_00.01.00.0
4multi lambda=04imat_fit_pos_condition_2_10.01.00.0
5multi lambda=05imat_fit_neg_condition_2_10.01.00.0
6multi lambda=06regularization_edge_has_flux_OR10.00.00.0
7multi lambda=0.50objective_condition_1__BIOMASS-10.01.0-10.0
8multi lambda=0.51objective_condition_2__BIOMASS-10.01.0-10.0
9multi lambda=0.52imat_fit_pos_condition_1_00.01.00.0
10multi lambda=0.53imat_fit_neg_condition_1_00.01.00.0
11multi lambda=0.54imat_fit_pos_condition_2_10.41.00.4
12multi lambda=0.55imat_fit_neg_condition_2_10.41.00.4
13multi lambda=0.56regularization_edge_has_flux_OR6.00.53.0
14single c1 lambda=0.50objective_condition_1__BIOMASS-10.01.0-10.0
15single c1 lambda=0.51imat_fit_pos_condition_1_00.01.00.0
16single c1 lambda=0.52imat_fit_neg_condition_1_00.01.00.0
17single c1 lambda=0.53regularization_edge_has_flux6.00.53.0
18single c2 lambda=0.50objective_condition_2__BIOMASS-10.01.0-10.0
19single c2 lambda=0.51imat_fit_pos_condition_2_00.01.00.0
20single c2 lambda=0.52imat_fit_neg_condition_2_00.01.00.0
21single c2 lambda=0.53regularization_edge_has_flux6.00.53.0
\n", - "
" - ], - "text/plain": [ - " setting term_index term_name value \\\n", - "0 multi lambda=0 0 objective_condition_1__BIOMASS -10.0 \n", - "1 multi lambda=0 1 objective_condition_2__BIOMASS -10.0 \n", - "2 multi lambda=0 2 imat_fit_pos_condition_1_0 0.0 \n", - "3 multi lambda=0 3 imat_fit_neg_condition_1_0 0.0 \n", - "4 multi lambda=0 4 imat_fit_pos_condition_2_1 0.0 \n", - "5 multi lambda=0 5 imat_fit_neg_condition_2_1 0.0 \n", - "6 multi lambda=0 6 regularization_edge_has_flux_OR 10.0 \n", - "7 multi lambda=0.5 0 objective_condition_1__BIOMASS -10.0 \n", - "8 multi lambda=0.5 1 objective_condition_2__BIOMASS -10.0 \n", - "9 multi lambda=0.5 2 imat_fit_pos_condition_1_0 0.0 \n", - "10 multi lambda=0.5 3 imat_fit_neg_condition_1_0 0.0 \n", - "11 multi lambda=0.5 4 imat_fit_pos_condition_2_1 0.4 \n", - "12 multi lambda=0.5 5 imat_fit_neg_condition_2_1 0.4 \n", - "13 multi lambda=0.5 6 regularization_edge_has_flux_OR 6.0 \n", - "14 single c1 lambda=0.5 0 objective_condition_1__BIOMASS -10.0 \n", - "15 single c1 lambda=0.5 1 imat_fit_pos_condition_1_0 0.0 \n", - "16 single c1 lambda=0.5 2 imat_fit_neg_condition_1_0 0.0 \n", - "17 single c1 lambda=0.5 3 regularization_edge_has_flux 6.0 \n", - "18 single c2 lambda=0.5 0 objective_condition_2__BIOMASS -10.0 \n", - "19 single c2 lambda=0.5 1 imat_fit_pos_condition_2_0 0.0 \n", - "20 single c2 lambda=0.5 2 imat_fit_neg_condition_2_0 0.0 \n", - "21 single c2 lambda=0.5 3 regularization_edge_has_flux 6.0 \n", - "\n", - " weight weighted_value \n", - "0 1.0 -10.0 \n", - "1 1.0 -10.0 \n", - "2 1.0 0.0 \n", - "3 1.0 0.0 \n", - "4 1.0 0.0 \n", - "5 1.0 0.0 \n", - "6 0.0 0.0 \n", - "7 1.0 -10.0 \n", - "8 1.0 -10.0 \n", - "9 1.0 0.0 \n", - "10 1.0 0.0 \n", - "11 1.0 0.4 \n", - "12 1.0 0.4 \n", - "13 0.5 3.0 \n", - "14 1.0 -10.0 \n", - "15 1.0 0.0 \n", - "16 1.0 0.0 \n", - "17 0.5 3.0 \n", - "18 1.0 -10.0 \n", - "19 1.0 0.0 \n", - "20 1.0 0.0 \n", - "21 0.5 3.0 " - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "solve-joint", + "metadata": {}, + "outputs": [], "source": [ - "def objectives_table(problem, setting):\n", - " rows = []\n", - " for i, (obj, weight) in enumerate(zip(problem.objectives, problem.weights)):\n", - " name = obj.name if obj.name else f\"unnamed_{i}\"\n", - " w = float(weight.value) if hasattr(weight, \"value\") else float(weight)\n", - " rows.append(\n", - " {\n", - " \"setting\": setting,\n", - " \"term_index\": i,\n", - " \"term_name\": name,\n", - " \"value\": float(obj.value),\n", - " \"weight\": w,\n", - " \"weighted_value\": float(obj.value) * w,\n", - " }\n", - " )\n", - " return pd.DataFrame(rows)\n", - "\n", + "joint_problem = MultiSampleIMAT(\n", + " eps=eps,\n", + " lambda_reg=lambda_reg,\n", + " scale=True,\n", + ").build_many(\n", + " G,\n", + " reaction_scores=reaction_scores,\n", + " reaction_bounds=reaction_bounds,\n", + ")\n", + "joint_problem.solve(solver=\"highs\")\n", "\n", - "obj_table = pd.concat(\n", - " [\n", - " objectives_table(P_multi_uncoupled, \"multi lambda=0\"),\n", - " objectives_table(P_multi_coupled, \"multi lambda=0.5\"),\n", - " objectives_table(P_single_c1, \"single c1 lambda=0.5\"),\n", - " objectives_table(P_single_c2, \"single c2 lambda=0.5\"),\n", - " ],\n", - " ignore_index=True,\n", + "joint_fluxes = pd.DataFrame(\n", + " np.asarray(joint_problem.expr.flow.value),\n", + " index=reaction_ids,\n", + " columns=condition_names,\n", ")\n", + "joint_activity = joint_fluxes.abs() > activity_tolerance\n", "\n", - "obj_table" + "assert joint_problem.expr.flow.value.shape == (G.num_edges, len(condition_names))" + ] + }, + { + "cell_type": "markdown", + "id": "fit-expression", + "metadata": {}, + "source": [ + "### Inspect reaction-level expression agreement\n", + "\n", + "Each condition must reach its fixed biomass. A positive reaction score is fitted when the reaction is active; a negative score is fitted when it is inactive. In the [original iMAT formulation](https://www.nature.com/articles/nbt.1487), these agreements contribute to a discrete consistency score. They are better described as **fits and mismatches** than as continuous residuals." ] }, { "cell_type": "code", - "execution_count": 7, - "id": "8816ca3b", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.993145Z", - "iopub.status.busy": "2026-02-15T11:15:36.993067Z", - "iopub.status.idle": "2026-02-15T11:15:36.996793Z", - "shell.execute_reply": "2026-02-15T11:15:36.996529Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
settingcondition_1_penaltycondition_2_penalty
0multi lambda=00.00.0
1multi lambda=0.50.00.8
2single lambda=0.50.00.0
\n", - "
" - ], - "text/plain": [ - " setting condition_1_penalty condition_2_penalty\n", - "0 multi lambda=0 0.0 0.0\n", - "1 multi lambda=0.5 0.0 0.8\n", - "2 single lambda=0.5 0.0 0.0" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "fit-summary", + "metadata": {}, + "outputs": [], "source": [ - "def fit_penalty_for_sample(problem, sample_name):\n", - " sample_name = str(sample_name).replace(\" \", \"_\")\n", - " sample_marker = f\"_{sample_name}_\"\n", - " return float(\n", - " sum(\n", - " float(o.value)\n", - " for o in problem.objectives\n", - " if o.name and o.name.startswith(\"imat_fit_\") and sample_marker in o.name\n", - " )\n", + "fit_rows = []\n", + "fit_details = []\n", + "\n", + "for condition in condition_names:\n", + " scores = pd.Series(reaction_scores[condition])\n", + " joint_scored_activity = joint_activity.loc[scores.index, condition]\n", + " independent_scored_activity = independent_activity.loc[scores.index, condition]\n", + " joint_fitted = ((scores > 0) & joint_scored_activity) | ((scores < 0) & ~joint_scored_activity)\n", + " independent_fitted = ((scores > 0) & independent_scored_activity) | (\n", + " (scores < 0) & ~independent_scored_activity\n", " )\n", "\n", + " for reaction_id in scores.index:\n", + " fit_details.append(\n", + " {\n", + " \"condition\": condition,\n", + " \"reaction\": reaction_id,\n", + " \"reaction score\": scores[reaction_id],\n", + " \"expected\": \"active\" if scores[reaction_id] > 0 else \"inactive\",\n", + " \"joint flux\": joint_fluxes.loc[reaction_id, condition],\n", + " \"joint state\": \"active\" if joint_scored_activity[reaction_id] else \"inactive\",\n", + " \"fitted independently\": bool(independent_fitted[reaction_id]),\n", + " \"fitted jointly\": bool(joint_fitted[reaction_id]),\n", + " }\n", + " )\n", "\n", - "fit_table = pd.DataFrame(\n", - " [\n", - " {\n", - " \"setting\": \"multi lambda=0\",\n", - " \"condition_1_penalty\": fit_penalty_for_sample(P_multi_uncoupled, \"condition_1\"),\n", - " \"condition_2_penalty\": fit_penalty_for_sample(P_multi_uncoupled, \"condition_2\"),\n", - " },\n", + " fit_rows.append(\n", " {\n", - " \"setting\": \"multi lambda=0.5\",\n", - " \"condition_1_penalty\": fit_penalty_for_sample(P_multi_coupled, \"condition_1\"),\n", - " \"condition_2_penalty\": fit_penalty_for_sample(P_multi_coupled, \"condition_2\"),\n", - " },\n", - " {\n", - " \"setting\": \"single lambda=0.5\",\n", - " \"condition_1_penalty\": fit_penalty_for_sample(P_single_c1, \"condition_1\"),\n", - " \"condition_2_penalty\": fit_penalty_for_sample(P_single_c2, \"condition_2\"),\n", - " },\n", - " ]\n", - ")\n", + " \"condition\": condition,\n", + " \"biomass\": joint_fluxes.loc[biomass_id, condition],\n", + " \"active reactions\": int(joint_activity[condition].sum()),\n", + " \"evidence fitted\": f\"{int(joint_fitted.sum())}/{len(scores)}\",\n", + " \"not fitted\": int((~joint_fitted).sum()),\n", + " }\n", + " )\n", "\n", - "fit_table" + " assert np.isclose(joint_fluxes.loc[biomass_id, condition], biomass_targets[condition])\n", + "\n", + "fit_summary = pd.DataFrame(fit_rows).set_index(\"condition\")\n", + "fit_details = pd.DataFrame(fit_details).set_index([\"reaction\", \"condition\"])\n", + "fit_summary" ] }, { "cell_type": "markdown", - "id": "81711827", + "id": "fit-matrix-intro", "metadata": {}, "source": [ - "## Shared reactions" + "A condition-by-reaction agreement matrix is especially useful in multi-condition analyses. It separates reactions that were not scored from scored reactions that the network could or could not fit." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "374ff582", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:36.998101Z", - "iopub.status.busy": "2026-02-15T11:15:36.998016Z", - "iopub.status.idle": "2026-02-15T11:15:37.002805Z", - "shell.execute_reply": "2026-02-15T11:15:37.002594Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
settingselected_branch
0multi lambda=0 (condition_1)branch_1 (A->B->C)
1multi lambda=0 (condition_2)branch_2 (A->D->C)
2multi lambda=0.5 (condition_1)branch_1 (A->B->C)
3multi lambda=0.5 (condition_2)branch_1 (A->B->C)
4single condition_1 lambda=0.5branch_1 (A->B->C)
5single condition_2 lambda=0.5branch_2 (A->D->C)
\n", - "
" - ], - "text/plain": [ - " setting selected_branch\n", - "0 multi lambda=0 (condition_1) branch_1 (A->B->C)\n", - "1 multi lambda=0 (condition_2) branch_2 (A->D->C)\n", - "2 multi lambda=0.5 (condition_1) branch_1 (A->B->C)\n", - "3 multi lambda=0.5 (condition_2) branch_1 (A->B->C)\n", - "4 single condition_1 lambda=0.5 branch_1 (A->B->C)\n", - "5 single condition_2 lambda=0.5 branch_2 (A->D->C)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "fit-matrix", + "metadata": {}, + "outputs": [], "source": [ - "idx = {rid: next(iter(G.get_edges_by_attr(\"id\", rid))) for rid in [\"R_AB\", \"R_AD\"]}\n", + "all_scored_reactions = score_table.index\n", + "fit_matrix = pd.DataFrame(\"not scored\", index=all_scored_reactions, columns=condition_names)\n", "\n", + "for (reaction_id, condition), row in fit_details.iterrows():\n", + " fit_matrix.loc[reaction_id, condition] = \"fitted\" if row[\"fitted jointly\"] else \"not fitted\"\n", "\n", - "def branch_label(ab_flux, ad_flux, tol=1e-6):\n", - " ab_on = abs(ab_flux) > tol\n", - " ad_on = abs(ad_flux) > tol\n", - " if ab_on and not ad_on:\n", - " return \"branch_1 (A->B->C)\"\n", - " if ad_on and not ab_on:\n", - " return \"branch_2 (A->D->C)\"\n", - " if ab_on and ad_on:\n", - " return \"both\"\n", - " return \"none\"\n", + "fit_matrix" + ] + }, + { + "cell_type": "markdown", + "id": "mismatch-interpretation", + "metadata": {}, + "source": [ + "The mismatch table should be examined before interpreting shared and condition-specific pathways. A reaction mismatched both independently and jointly points to expression–feasibility tension within that condition. A reaction fitted independently but mismatched only in the joint model instead points to a trade-off introduced by shared regularization.\n", "\n", + "Expression is evidence about the likelihood of reaction activity, not a measurement of activity or flux. The [original iMAT paper](https://www.nature.com/articles/nbt.1487) reports a central role for post-transcriptional regulation and uses disagreements between expression and predicted activity to generate hypotheses about regulation beyond transcript abundance. Possible mechanisms include protein translation or degradation, phosphorylation and other post-translational effects, and metabolite-level control such as substrate availability and allosteric regulation. A disagreement can also arise from expression thresholds, GPR mapping, missing or incorrect model reactions, medium composition, or an imposed phenotype. iMAT alone cannot distinguish these explanations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mismatched-reactions", + "metadata": {}, + "outputs": [], + "source": [ + "mismatches = fit_details[~fit_details[\"fitted jointly\"]].copy()\n", "\n", - "branch_usage = pd.DataFrame(\n", - " {\n", - " \"setting\": [\n", - " \"multi lambda=0 (condition_1)\",\n", - " \"multi lambda=0 (condition_2)\",\n", - " \"multi lambda=0.5 (condition_1)\",\n", - " \"multi lambda=0.5 (condition_2)\",\n", - " \"single condition_1 lambda=0.5\",\n", - " \"single condition_2 lambda=0.5\",\n", - " ],\n", - " \"selected_branch\": [\n", - " branch_label(\n", - " P_multi_uncoupled.expr.flow[idx[\"R_AB\"], 0].value, P_multi_uncoupled.expr.flow[idx[\"R_AD\"], 0].value\n", - " ),\n", - " branch_label(\n", - " P_multi_uncoupled.expr.flow[idx[\"R_AB\"], 1].value, P_multi_uncoupled.expr.flow[idx[\"R_AD\"], 1].value\n", - " ),\n", - " branch_label(\n", - " P_multi_coupled.expr.flow[idx[\"R_AB\"], 0].value, P_multi_coupled.expr.flow[idx[\"R_AD\"], 0].value\n", - " ),\n", - " branch_label(\n", - " P_multi_coupled.expr.flow[idx[\"R_AB\"], 1].value, P_multi_coupled.expr.flow[idx[\"R_AD\"], 1].value\n", - " ),\n", - " branch_label(P_single_c1.expr.flow[idx[\"R_AB\"]].value, P_single_c1.expr.flow[idx[\"R_AD\"]].value),\n", - " branch_label(P_single_c2.expr.flow[idx[\"R_AB\"]].value, P_single_c2.expr.flow[idx[\"R_AD\"]].value),\n", - " ],\n", - " }\n", - ")\n", + "assert list(mismatches.index) == [(\"PFL\", \"glucose anaerobic\")]\n", + "assert not mismatches.iloc[0][\"fitted independently\"]\n", + "mismatches" + ] + }, + { + "cell_type": "markdown", + "id": "pfl-mismatch", + "metadata": {}, + "source": [ + "`PFL` is the only unmatched reaction. It is classified as low but remains active in both the independent and joint anaerobic solutions. This is not caused by cross-condition coupling: under the imposed anaerobic growth phenotype and high ethanol evidence, the selected network uses `PFL` to supply acetyl-CoA and formate.\n", + "\n", + "In real data, this would be a hypothesis-generating result—not proof that `PFL` is active despite low expression, and not proof of post-translational regulation. The first checks should separate two broad possibilities:\n", "\n", - "single_c1_branch = branch_usage.loc[\n", - " branch_usage[\"setting\"] == \"single condition_1 lambda=0.5\", \"selected_branch\"\n", - "].item()\n", - "single_c2_branch = branch_usage.loc[\n", - " branch_usage[\"setting\"] == \"single condition_2 lambda=0.5\", \"selected_branch\"\n", - "].item()\n", - "multi_c1_branch = branch_usage.loc[\n", - " branch_usage[\"setting\"] == \"multi lambda=0.5 (condition_1)\", \"selected_branch\"\n", - "].item()\n", - "multi_c2_branch = branch_usage.loc[\n", - " branch_usage[\"setting\"] == \"multi lambda=0.5 (condition_2)\", \"selected_branch\"\n", - "].item()\n", - "\n", - "assert single_c1_branch != single_c2_branch, \"Single-sample runs should select different branches.\"\n", - "assert multi_c1_branch == multi_c2_branch, \"Coupled multi-sample run should align branch usage.\"\n", - "\n", - "branch_usage" + "- **biology not captured by transcript abundance**, including enzyme abundance or modification and metabolite-level regulation;\n", + "- **data or model assumptions**, including thresholds, GPR rules, network completeness, medium bounds, and the required phenotype.\n", + "\n", + "Proteomics, enzyme-activity, metabolomics, or flux measurements are needed to discriminate among these explanations." ] }, { - "cell_type": "code", - "execution_count": 9, - "id": "3f066a68", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:37.003910Z", - "iopub.status.busy": "2026-02-15T11:15:37.003842Z", - "iopub.status.idle": "2026-02-15T11:15:37.007924Z", - "shell.execute_reply": "2026-02-15T11:15:37.007671Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
settingn_sharedsharedonly_condition_1only_condition_2
0multi lambda=0.56[BIOMASS, EX_A, R_AB, R_BC, R_CE, R_EF][][]
1single lambda=0.54[BIOMASS, EX_A, R_CE, R_EF][R_AB, R_BC][R_AD, R_DC]
\n", - "
" - ], - "text/plain": [ - " setting n_shared shared \\\n", - "0 multi lambda=0.5 6 [BIOMASS, EX_A, R_AB, R_BC, R_CE, R_EF] \n", - "1 single lambda=0.5 4 [BIOMASS, EX_A, R_CE, R_EF] \n", - "\n", - " only_condition_1 only_condition_2 \n", - "0 [] [] \n", - "1 [R_AB, R_BC] [R_AD, R_DC] " - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "cell_type": "markdown", + "id": "joint-versus-independent", + "metadata": {}, "source": [ - "all_edge_ids = [G.get_attr_edge(i).get(\"id\") for i in range(G.num_edges)]\n", - "\n", - "\n", - "def active_ids(problem, condition=None, tol=1e-6):\n", - " flow = np.asarray(problem.expr.flow.value)\n", - " if condition is not None:\n", - " flow = flow[:, condition]\n", - " return {all_edge_ids[i] for i in np.where(np.abs(flow) > tol)[0]}\n", - "\n", - "\n", - "def shared_unique(active_c1, active_c2, setting):\n", - " shared = sorted(active_c1 & active_c2)\n", - " only_c1 = sorted(active_c1 - active_c2)\n", - " only_c2 = sorted(active_c2 - active_c1)\n", - " return {\n", - " \"setting\": setting,\n", - " \"n_shared\": len(shared),\n", - " \"shared\": shared,\n", - " \"only_condition_1\": only_c1,\n", - " \"only_condition_2\": only_c2,\n", - " }\n", + "### What did joint inference change?\n", "\n", + "The independent solutions are compared only after both optimizations have finished. The joint solution considers reuse while selecting the flux states, producing a smaller union and a larger internally consistent shared set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "union-comparison", + "metadata": {}, + "outputs": [], + "source": [ + "independent_union = independent_activity.any(axis=1)\n", + "independent_shared = independent_activity.all(axis=1)\n", + "joint_union = joint_activity.any(axis=1)\n", + "joint_shared = joint_activity.all(axis=1)\n", "\n", - "shared_table = pd.DataFrame(\n", - " [\n", - " shared_unique(active_ids(P_multi_coupled, 0), active_ids(P_multi_coupled, 1), \"multi lambda=0.5\"),\n", - " shared_unique(active_ids(P_single_c1), active_ids(P_single_c2), \"single lambda=0.5\"),\n", - " ]\n", + "selection_comparison = pd.DataFrame(\n", + " {\n", + " \"shared reactions\": {\n", + " \"independent inference, compared afterward\": int(independent_shared.sum()),\n", + " \"one joint inference\": int(joint_shared.sum()),\n", + " },\n", + " \"reaction union\": {\n", + " \"independent inference, compared afterward\": int(independent_union.sum()),\n", + " \"one joint inference\": int(joint_union.sum()),\n", + " },\n", + " }\n", ")\n", "\n", - "shared_table" + "assert joint_union.sum() < independent_union.sum()\n", + "assert joint_shared.sum() > independent_shared.sum()\n", + "selection_comparison" ] }, { - "cell_type": "code", - "execution_count": 10, - "id": "0ec4a31d", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:37.008954Z", - "iopub.status.busy": "2026-02-15T11:15:37.008893Z", - "iopub.status.idle": "2026-02-15T11:15:37.020063Z", - "shell.execute_reply": "2026-02-15T11:15:37.019772Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "digraph {\n", - "\tgraph [center=1 rankdir=LR]\n", - "\tnode [fixedsize=true]\n", - "\te_0_source [shape=point]\n", - "\tA [shape=circle]\n", - "\tA [shape=circle]\n", - "\tB [shape=circle]\n", - "\tB [shape=circle]\n", - "\tC [shape=circle]\n", - "\tC [shape=circle]\n", - "\tE [shape=circle]\n", - "\tE [shape=circle]\n", - "\tF [shape=circle]\n", - "\te_5_target [shape=point]\n", - "\tF [shape=circle]\n", - "\te_0_source -> A [arrowhead=normal]\n", - "\tA -> B [arrowhead=normal]\n", - "\tB -> C [arrowhead=normal]\n", - "\tC -> E [arrowhead=normal]\n", - "\tE -> F [arrowhead=normal]\n", - "\tF -> e_5_target [arrowhead=normal]\n", - "}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "cell_type": "markdown", + "id": "usage-patterns", + "metadata": {}, "source": [ - "active_multi_c1 = np.abs(P_multi_coupled.expr.flow.value[:, 0]) > 1e-6\n", - "active_multi_c2 = np.abs(P_multi_coupled.expr.flow.value[:, 1]) > 1e-6\n", + "## Shared and condition-specific metabolism\n", "\n", - "G.edge_subgraph(active_multi_c1).plot(graph_attr={\"rankdir\": \"LR\", \"center\": \"1\"})" + "As in Multi-condition FBA, categories are derived from the joint activity matrix. With two conditions, every reaction in the union is either shared, aerobic-specific, or anaerobic-specific." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "1a42f91d", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:37.021194Z", - "iopub.status.busy": "2026-02-15T11:15:37.021107Z", - "iopub.status.idle": "2026-02-15T11:15:37.031511Z", - "shell.execute_reply": "2026-02-15T11:15:37.031205Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "digraph {\n", - "\tgraph [center=1 rankdir=LR]\n", - "\tnode [fixedsize=true]\n", - "\te_0_source [shape=point]\n", - "\tA [shape=circle]\n", - "\tA [shape=circle]\n", - "\tB [shape=circle]\n", - "\tB [shape=circle]\n", - "\tC [shape=circle]\n", - "\tC [shape=circle]\n", - "\tE [shape=circle]\n", - "\tE [shape=circle]\n", - "\tF [shape=circle]\n", - "\te_5_target [shape=point]\n", - "\tF [shape=circle]\n", - "\te_0_source -> A [arrowhead=normal]\n", - "\tA -> B [arrowhead=normal]\n", - "\tB -> C [arrowhead=normal]\n", - "\tC -> E [arrowhead=normal]\n", - "\tE -> F [arrowhead=normal]\n", - "\tF -> e_5_target [arrowhead=normal]\n", - "}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "categorize-reactions", + "metadata": {}, + "outputs": [], + "source": [ + "reaction_groups = pd.Series(\"inactive\", index=reaction_ids, name=\"usage group\")\n", + "reaction_groups.loc[joint_shared] = \"shared\"\n", + "reaction_groups.loc[joint_activity[\"glucose aerobic\"] & ~joint_activity[\"glucose anaerobic\"]] = (\n", + " \"glucose aerobic only\"\n", + ")\n", + "reaction_groups.loc[~joint_activity[\"glucose aerobic\"] & joint_activity[\"glucose anaerobic\"]] = (\n", + " \"glucose anaerobic only\"\n", + ")\n", + "\n", + "group_table = (\n", + " reaction_groups[joint_union]\n", + " .groupby(reaction_groups[joint_union], sort=False)\n", + " .agg(\n", + " reactions=\"size\",\n", + " reaction_ids=lambda values: \", \".join(values.index),\n", + " )\n", + ")\n", + "\n", + "assert int(group_table[\"reactions\"].sum()) == int(joint_union.sum())\n", + "group_table" + ] + }, + { + "cell_type": "markdown", + "id": "biological-interpretation", + "metadata": {}, "source": [ - "G.edge_subgraph(active_multi_c2).plot(graph_attr={\"rankdir\": \"LR\", \"center\": \"1\"})" + "### Biological interpretation\n", + "\n", + "| Usage group | Characteristic reactions | Interpretation |\n", + "|---|---|---|\n", + "| Shared | `GLCpts`, `PFK`, `GAPD`, `ENO`, `PYK`, biomass | Central glucose utilization and precursor production required in both states |\n", + "| Glucose aerobic only | `EX_o2_e`, `O2t`, `CYTBD`, `NADH16`, `PDH` | Oxygen uptake, respiratory electron transfer, and oxidative pyruvate metabolism |\n", + "| Glucose anaerobic only | `PFL`, `FORti`, `EX_for_e`, `ACALD`, `ALCD2x`, `ETOHt2r`, `EX_etoh_e` | Pyruvate-formate cleavage followed by formate and ethanol secretion |\n", + "\n", + "Most condition-specific reactions agree with the supplied evidence; `PFL` is the explicit exception identified by the mismatch analysis. The inferred sets also include the transport and exchange reactions required to complete each biological route. That is an advantage of network inference over listing high-expression reactions alone: the output is a stoichiometrically connected hypothesis that makes conflicting evidence visible.\n", + "\n", + "As noted in the Multi-condition FBA guide, shared activity does not imply equal magnitude or direction. For example, `ATPS4r` is active in both selected states but carries flux in opposite directions in this solution." ] }, { "cell_type": "markdown", - "id": "ae1c6d9a", + "id": "network-visualization", "metadata": {}, "source": [ - "## Manual check of objectives per condition" + "### Visualize the jointly inferred network\n", + "\n", + "The plot shows the complete joint union. Shared reactions are gray, aerobic-specific reactions are blue, and anaerobic-specific reactions are red.\n", + "\n", + "| Usage group | Color |\n", + "|---|---|\n", + "| Shared | Gray |\n", + "| Glucose aerobic only | Blue |\n", + "| Glucose anaerobic only | Red |" ] }, { "cell_type": "code", - "execution_count": 12, - "id": "3f372c79", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-15T11:15:37.032792Z", - "iopub.status.busy": "2026-02-15T11:15:37.032689Z", - "iopub.status.idle": "2026-02-15T11:15:37.037532Z", - "shell.execute_reply": "2026-02-15T11:15:37.037292Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
settingcondition_1_ratiocondition_2_ratio
0multi lambda=01.01.0
1multi lambda=0.51.00.2
2single lambda=0.51.01.0
\n", - "
" - ], - "text/plain": [ - " setting condition_1_ratio condition_2_ratio\n", - "0 multi lambda=0 1.0 1.0\n", - "1 multi lambda=0.5 1.0 0.2\n", - "2 single lambda=0.5 1.0 1.0" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "plot-joint-network", + "metadata": {}, + "outputs": [], "source": [ - "edge_index_by_id = {rid: i for i, rid in enumerate(all_edge_ids)}\n", + "group_colors = {\n", + " \"shared\": \"#b0b0b0\",\n", + " \"glucose aerobic only\": \"#277da1\",\n", + " \"glucose anaerobic only\": \"#e63946\",\n", + "}\n", + "\n", + "selected_reactions = np.flatnonzero(joint_union.to_numpy())\n", + "joint_network = G.edge_subgraph(selected_reactions)\n", + "edge_style = {}\n", "\n", + "for displayed_index, original_index in enumerate(selected_reactions):\n", + " reaction_id = reaction_ids[original_index]\n", + " group = reaction_groups[reaction_id]\n", + " edge_style[displayed_index] = {\n", + " \"color\": group_colors[group],\n", + " \"penwidth\": \"1.5\" if group == \"shared\" else \"4\",\n", + " }\n", + "\n", + "joint_network.plot(\n", + " graph_attr={\"rankdir\": \"LR\"},\n", + " node_attr={\n", + " \"fixedsize\": \"false\",\n", + " \"shape\": \"box\",\n", + " \"style\": \"rounded\",\n", + " \"margin\": \"0.05,0.03\",\n", + " },\n", + " custom_edge_attr=edge_style,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "interpretation-limitations", + "metadata": {}, + "source": [ + "## Interpretation and limitations\n", "\n", - "def manual_fit_ratio(problem, scores, condition=None, tol=1e-6):\n", - " flow = np.asarray(problem.expr.flow.value)\n", - " if condition is not None:\n", - " flow = flow[:, condition]\n", + "By fitting the conditions together, we can compare them against the same metabolic solution. The model reuses reactions when it can, but still keeps the respiratory pathway in the aerobic condition and fermentation in the anaerobic condition. This is different from averaging the expression profiles or comparing networks inferred in separate runs.\n", "\n", - " matched = 0.0\n", - " total = 0.0\n", - " for rid, score in scores.items():\n", - " i = edge_index_by_id[rid]\n", - " active = abs(float(flow[i])) > tol\n", - " weight = abs(float(score))\n", - " ok = (score > 0 and active) or (score < 0 and not active)\n", - " total += weight\n", - " if ok:\n", - " matched += weight\n", - " return matched / total if total > 0 else np.nan\n", + "The fit table is an important part of the biological interpretation. It shows where the expression data support the selected network and where the model cannot satisfy both the expression evidence and the metabolic constraints. Comparing the independent and joint fits also reveals whether a mismatch was already present in one condition or appeared when the conditions were analyzed together.\n", "\n", + "Keep in mind that RNA abundance does not directly measure enzyme activity or flux. Protein abundance, post-translational regulation, and metabolite-level control can all separate expression from metabolic activity. A mismatch is therefore something to investigate, not evidence for a particular regulatory mechanism.\n", "\n", - "manual_fit_table = pd.DataFrame(\n", - " [\n", - " {\n", - " \"setting\": \"multi lambda=0\",\n", - " \"condition_1_ratio\": manual_fit_ratio(P_multi_uncoupled, scores_condition_1, condition=0),\n", - " \"condition_2_ratio\": manual_fit_ratio(P_multi_uncoupled, scores_condition_2, condition=1),\n", - " },\n", - " {\n", - " \"setting\": \"multi lambda=0.5\",\n", - " \"condition_1_ratio\": manual_fit_ratio(P_multi_coupled, scores_condition_1, condition=0),\n", - " \"condition_2_ratio\": manual_fit_ratio(P_multi_coupled, scores_condition_2, condition=1),\n", - " },\n", - " {\n", - " \"setting\": \"single lambda=0.5\",\n", - " \"condition_1_ratio\": manual_fit_ratio(P_single_c1, scores_condition_1),\n", - " \"condition_2_ratio\": manual_fit_ratio(P_single_c2, scores_condition_2),\n", - " },\n", - " ]\n", - ")\n", + "In practice, it is worth checking different expression thresholds and comparing the joint result with the independently inferred networks. Shared reactions may still carry different fluxes—or even operate in opposite directions—and alternative solutions may exist. The choice of `lambda_reg` also matters: larger values favor more reaction reuse, but can eventually outweigh weaker expression evidence.\n", "\n", - "manual_fit_table" + "The synthetic profiles isolate the multi-condition formulation. For a workflow using measured expression data, continue with the [context-specific metabolic omics tutorial](../../tutorials/fba/context-specific-metabolic-omics.ipynb)." ] } ], "metadata": { "kernelspec": { - "display_name": "default", + "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.11" } }, "nbformat": 4, diff --git a/docs/guide/metabolism/multicondition-sfba.ipynb b/docs/guide/metabolism/multicondition-sfba.ipynb index 93cbd34b7..44b3f30d2 100644 --- a/docs/guide/metabolism/multicondition-sfba.ipynb +++ b/docs/guide/metabolism/multicondition-sfba.ipynb @@ -2,321 +2,558 @@ "cells": [ { "cell_type": "markdown", - "id": "mc_sfba_01", + "id": "multicondition-introduction", "metadata": {}, "source": [ - "# Multi-condition sparse FBA\n", + "# Multi-condition FBA\n", "\n", - "This tutorial extends the sparse FBA workflow from `flux-balance-analysis.ipynb` to multiple conditions. We first solve standard multi-condition FBA to get each condition's optimal biomass. Then, as in single-condition sparse FBA, we constrain biomass to at least 90% of optimum and minimize the number of active reactions.\n", + "Multi-condition flux balance analysis represents several metabolic contexts in one optimization problem. Every condition has its own flux vector, reaction bounds, and objectives, so media, perturbations, or required phenotypes can differ while the underlying metabolic model remains the same.\n", "\n", - "To make the effect visible, we use a toy network where each condition has a small preference for a different branch." + "In CORNETO, `MultiSampleFBA.build_many` constructs this condition-by-reaction flux matrix directly. This supports two related uses:\n", + "\n", + "- solve and inspect several condition-specific FBA problems through one consistent interface;\n", + "- couple reaction selection across conditions to identify a compact shared program and the reactions needed by particular contexts.\n", + "\n", + "The second use is the main advantage of the multi-condition formulation: all conditions influence reaction selection before the networks are compared." + ] + }, + { + "cell_type": "markdown", + "id": "independent-and-coupled", + "metadata": {}, + "source": [ + "## Independent and coupled formulations\n", + "\n", + "Without regularization, the condition-specific flux vectors obey separate steady-state constraints and can be interpreted as several FBA problems built together. With `lambda_reg > 0`, CORNETO adds structured sparsity across the complete flux matrix.\n", + "\n", + "For each reaction, CORNETO records whether it is active in each condition and whether it is active in the union. A reaction used by one or several conditions contributes once to the union-level penalty. Reusing a reaction is therefore cheaper than introducing another alternative, while condition-specific reactions remain available when required by their bounds or phenotypes.\n", + "\n", + "Structured sparsity penalizes the union; it does not force identical fluxes and does not directly maximize the intersection. Shared and context-specific reactions are summaries derived from the jointly selected condition networks." + ] + }, + { + "cell_type": "markdown", + "id": "joint-versus-pairwise", + "metadata": {}, + "source": [ + "## Why is this different from pairwise comparisons?\n", + "\n", + "A metabolic model usually admits many equivalent or near-equivalent flux distributions. Consider an experiment with a control and two perturbed conditions, A and B. One common strategy is to run two independent optimizations:\n", + "\n", + "| Optimization | Condition-specific flux solutions | Reactions coupled by one union penalty |\n", + "|---|---|---|\n", + "| Separate analysis 1 | control and condition A | reactions used by control or A |\n", + "| Separate analysis 2 | a new control solution and condition B | reactions used by control or B |\n", + "\n", + "The media are not combined: every condition keeps its own bounds and flux vector. However, the control is solved twice, once inside each independent optimization. Its **selected control reaction set**—the reactions carrying flux in that solution—can differ between the two runs even though its bounds and required phenotype are unchanged.\n", + "\n", + "Comparing the two results afterward can therefore mix genuine biological differences with independent selection among alternative pathways. In a joint CORNETO analysis, one optimization contains one flux vector for control, one for A, and one for B, and applies a single union penalty across all three. The resulting shared and condition-dependent reaction sets are internally consistent with the complete experiment." + ] + }, + { + "cell_type": "markdown", + "id": "cobrapy-comparison", + "metadata": {}, + "source": [ + "## Relation to COBRApy\n", + "\n", + "COBRApy naturally solves FBA or pFBA independently for each medium. A user can reproduce joint selection by manually constructing a coupled mixed-integer formulation with optlang. CORNETO provides the condition-specific flux matrix and union-level structured sparsity directly through `MultiSampleFBA.build_many`." + ] + }, + { + "cell_type": "markdown", + "id": "shared-context-example", + "metadata": {}, + "source": [ + "## Example: shared and context-specific metabolism\n", + "\n", + "The [FBA guide](flux-balance-analysis.ipynb) showed how to solve several media conditions in one CORNETO problem. Here we use that interface to address a small biological question:\n", + "\n", + "> Which reactions form a compact metabolic program shared across metabolic conditions, and which reactions are required only in particular contexts?\n", + "\n", + "We will compare aerobic growth on glucose (the control), aerobic growth on acetate, and anaerobic growth on glucose in the COBRApy *E. coli* core model. The conditions are deliberately simple: they illustrate joint reaction selection rather than reproduce a particular experiment. After solving them jointly, reactions can be described as:\n", + "\n", + "- **shared core**: active in all conditions;\n", + "- **partially shared**: active in more than one condition, but not all;\n", + "- **context-specific**: active in exactly one condition;\n", + "- **union**: active in at least one condition." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_02", + "id": "load-model", "metadata": {}, "outputs": [], "source": [ + "from contextlib import redirect_stdout\n", + "from io import StringIO\n", + "\n", "import numpy as np\n", "import pandas as pd\n", + "from cobra.io import load_model\n", "\n", - "import corneto as cn\n", + "from corneto.io import cobra_model_to_graph\n", "from corneto.methods import MultiSampleFBA\n", "\n", - "cn.info()" + "with redirect_stdout(StringIO()):\n", + " cobra_model = load_model(\"textbook\")\n", + "\n", + "G = cobra_model_to_graph(cobra_model)\n", + "reaction_ids = list(G.get_attr_from_edges(\"id\"))\n", + "biomass_id = \"Biomass_Ecoli_core\"\n", + "biomass_index = reaction_ids.index(biomass_id)" + ] + }, + { + "cell_type": "markdown", + "id": "oxygen-conditions", + "metadata": {}, + "source": [ + "## Define the metabolic conditions\n", + "\n", + "The conditions change either the carbon source or oxygen availability. As in the previous guide, uptake is a negative exchange flux and `0` blocks uptake.\n", + "\n", + "| Condition | Glucose uptake | Acetate uptake | Oxygen uptake |\n", + "|---|---:|---:|---:|\n", + "| Glucose aerobic | −10 | 0 | −20 |\n", + "| Acetate aerobic | 0 | −10 | −20 |\n", + "| Glucose anaerobic | −10 | 0 | 0 |" ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_03", + "id": "condition-inputs", "metadata": {}, "outputs": [], "source": [ - "G = cn.Graph()\n", + "condition_names = [\"glucose aerobic\", \"acetate aerobic\", \"glucose anaerobic\"]\n", "\n", - "# Uptake\n", - "G.add_edge((), \"A\", id=\"EX_A\", default_lb=0, default_ub=10)\n", - "\n", - "# Two alternative branches\n", - "G.add_edge(\"A\", \"B\", id=\"R_AB\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"B\", \"C\", id=\"R_BC\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"A\", \"D\", id=\"R_AD\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"D\", \"C\", id=\"R_DC\", default_lb=0, default_ub=100)\n", - "\n", - "# Shared pathway to biomass\n", - "G.add_edge(\"C\", \"E\", id=\"R_CE\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"E\", \"F\", id=\"R_EF\", default_lb=0, default_ub=100)\n", - "G.add_edge(\"F\", (), id=\"BIOMASS\", default_lb=0, default_ub=100)\n", + "media_bounds = {\n", + " \"glucose aerobic\": {\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_ac_e\": (0.0, 1000.0),\n", + " \"EX_o2_e\": (-20.0, 1000.0),\n", + " },\n", + " \"acetate aerobic\": {\n", + " \"EX_glc__D_e\": (0.0, 1000.0),\n", + " \"EX_ac_e\": (-10.0, 1000.0),\n", + " \"EX_o2_e\": (-20.0, 1000.0),\n", + " },\n", + " \"glucose anaerobic\": {\n", + " \"EX_glc__D_e\": (-10.0, 1000.0),\n", + " \"EX_ac_e\": (0.0, 1000.0),\n", + " \"EX_o2_e\": (0.0, 1000.0),\n", + " },\n", + "}\n", + "biomass_objectives = {name: {biomass_id: -1} for name in condition_names}" + ] + }, + { + "cell_type": "markdown", + "id": "maximum-biomass", + "metadata": {}, + "source": [ + "## Establish each condition's growth capacity\n", "\n", - "pd.DataFrame(\n", - " {\n", - " \"edge_index\": range(G.num_edges),\n", - " \"reaction_id\": [G.get_attr_edge(i).get(\"id\") for i in range(G.num_edges)],\n", - " }\n", - ")" + "First, standard multi-condition FBA determines the maximum biomass supported by each medium. At this stage the flux vectors share one problem object but are not coupled by sparsity." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_04", + "id": "solve-maximum-biomass", "metadata": {}, "outputs": [], "source": [ - "G.plot()" + "standard_problem = MultiSampleFBA().build_many(\n", + " G,\n", + " objectives=biomass_objectives,\n", + " reaction_bounds=media_bounds,\n", + ")\n", + "standard_problem.solve(solver=\"scipy\")\n", + "\n", + "maximum_biomass = pd.Series(\n", + " standard_problem.expr.flow.value[biomass_index, :],\n", + " index=condition_names,\n", + " name=\"maximum biomass\",\n", + ")\n", + "\n", + "assert (maximum_biomass > 0).all()\n", + "maximum_biomass.to_frame()" + ] + }, + { + "cell_type": "markdown", + "id": "sparse-setup", + "metadata": {}, + "source": [ + "## Require growth and favor a compact reaction union\n", + "\n", + "For this controlled comparison, biomass is fixed at exactly 90% of each condition's optimum. Pairwise and joint analyses therefore explain the same predefined phenotypes: network differences cannot be attributed to different achieved growth rates.\n", + "\n", + "`lambda_reg=0.1` penalizes reactions in the union of the selected condition networks. Because biomass is fixed, any positive regularization weight ranks feasible solutions by their union size instead of trading additional growth for sparsity.\n", + "\n", + "The two helpers below keep the scientific inputs visible: one builds a sparse problem for named conditions and the other labels its flux matrix." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_05", + "id": "sparse-helpers", "metadata": {}, "outputs": [], "source": [ - "def make_condition(preferred_branch):\n", - " # Main objective: maximize biomass (negative value because objectives are minimized).\n", - " sample = {\"BIOMASS\": {\"role\": \"objective\", \"value\": -1.0}}\n", - " # Small condition-specific preference for one branch.\n", - " sample[preferred_branch] = {\"role\": \"objective\", \"value\": -0.01}\n", - " return sample\n", + "biomass_fraction = 0.90\n", + "biomass_targets = biomass_fraction * maximum_biomass\n", + "activity_tolerance = 1e-6\n", "\n", "\n", - "condition_specs = {\n", - " \"condition_1\": make_condition(\"R_BC\"),\n", - " \"condition_2\": make_condition(\"R_DC\"),\n", - "}\n", + "def build_sparse_problem(names):\n", + " \"\"\"Build and solve sparse FBA for the requested conditions.\"\"\"\n", + " objectives = {name: biomass_objectives[name] for name in names}\n", + " bounds = {\n", + " name: {\n", + " **media_bounds[name],\n", + " biomass_id: (biomass_targets[name], biomass_targets[name]),\n", + " }\n", + " for name in names\n", + " }\n", + " problem = MultiSampleFBA(lambda_reg=0.1).build_many(\n", + " G,\n", + " objectives=objectives,\n", + " reaction_bounds=bounds,\n", + " )\n", + " problem.solve(solver=\"highs\")\n", + " return problem\n", + "\n", "\n", - "multi_data = cn.Data.from_cdict(condition_specs)\n", - "sample_names = list(condition_specs)" + "def labeled_fluxes(problem, names):\n", + " \"\"\"Return the solved flux matrix with reaction and condition labels.\"\"\"\n", + " return pd.DataFrame(\n", + " problem.expr.flow.value,\n", + " index=reaction_ids,\n", + " columns=names,\n", + " )" ] }, { "cell_type": "markdown", - "id": "mc_sfba_06", + "id": "separate-comparisons", "metadata": {}, "source": [ - "## Standard multi-condition FBA\n", + "## What happens in separate two-condition analyses?\n", "\n", - "We start with regular multi-condition FBA to obtain the maximal biomass in each condition. These values are later used as lower bounds for sparse FBA." + "First we build one optimization containing glucose-aerobic and acetate-aerobic flux vectors. Separately, we build another containing glucose-aerobic and glucose-anaerobic flux vectors. Each condition retains its own medium bounds; they are columns in the same optimization, not a combined medium.\n", + "\n", + "Because these are independent optimizations, glucose aerobic is solved twice. We compare the active reactions selected for that identical glucose-aerobic condition in the two results." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_07", + "id": "solve-pairs", "metadata": {}, "outputs": [], "source": [ - "idx = {rid: next(iter(G.get_edges_by_attr(\"id\", rid))) for rid in [\"BIOMASS\", \"R_AB\", \"R_AD\"]}\n", + "control_acetate_names = [\"glucose aerobic\", \"acetate aerobic\"]\n", + "control_anaerobic_names = [\"glucose aerobic\", \"glucose anaerobic\"]\n", "\n", - "P_opt = MultiSampleFBA(lambda_reg=0.0).build(G, multi_data)\n", - "P_opt.solve(solver=\"highs\")\n", + "control_acetate_problem = build_sparse_problem(control_acetate_names)\n", + "control_anaerobic_problem = build_sparse_problem(control_anaerobic_names)\n", "\n", - "biomass_opt = {name: float(P_opt.expr.flow[idx[\"BIOMASS\"], i].value) for i, name in enumerate(sample_names)}\n", + "control_acetate_fluxes = labeled_fluxes(control_acetate_problem, control_acetate_names)\n", + "control_anaerobic_fluxes = labeled_fluxes(control_anaerobic_problem, control_anaerobic_names)\n", "\n", - "pd.DataFrame(\n", + "pairwise_control_summary = pd.DataFrame(\n", " {\n", - " \"condition\": sample_names,\n", - " \"optimal_biomass\": [biomass_opt[name] for name in sample_names],\n", + " \"analyzed with acetate aerobic\": {\n", + " \"biomass\": control_acetate_fluxes.loc[biomass_id, \"glucose aerobic\"],\n", + " \"active reactions\": (\n", + " control_acetate_fluxes[\"glucose aerobic\"].abs() > activity_tolerance\n", + " ).sum(),\n", + " },\n", + " \"analyzed with glucose anaerobic\": {\n", + " \"biomass\": control_anaerobic_fluxes.loc[biomass_id, \"glucose aerobic\"],\n", + " \"active reactions\": (\n", + " control_anaerobic_fluxes[\"glucose aerobic\"].abs() > activity_tolerance\n", + " ).sum(),\n", + " },\n", " }\n", - ")" + ")\n", + "\n", + "assert np.isclose(pairwise_control_summary.loc[\"biomass\"].iloc[0], pairwise_control_summary.loc[\"biomass\"].iloc[1])\n", + "pairwise_control_summary" ] }, { - "cell_type": "markdown", - "id": "mc_sfba_08", + "cell_type": "code", + "execution_count": null, + "id": "compare-controls", "metadata": {}, + "outputs": [], "source": [ - "## Sparse FBA per condition (independent runs)\n", + "control_activity = pd.DataFrame(\n", + " {\n", + " \"analyzed with acetate aerobic\": (\n", + " control_acetate_fluxes[\"glucose aerobic\"].abs() > activity_tolerance\n", + " ),\n", + " \"analyzed with glucose anaerobic\": (\n", + " control_anaerobic_fluxes[\"glucose aerobic\"].abs() > activity_tolerance\n", + " ),\n", + " }\n", + ")\n", "\n", - "This mirrors the single-condition sparse FBA pattern from `flux-balance-analysis.ipynb`:\n", + "different_control_reactions = control_activity[\n", + " control_activity.iloc[:, 0] != control_activity.iloc[:, 1]\n", + "].copy()\n", + "different_control_reactions.insert(0, \"reaction\", different_control_reactions.index)\n", "\n", - "1. Solve standard FBA to get optimum biomass.\n", - "2. Add a biomass lower bound (90% of optimum).\n", - "3. Solve sparse FBA (`lambda_reg > 0`)." + "assert not different_control_reactions.empty\n", + "different_control_reactions.reset_index(drop=True)" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "mc_sfba_09", + "cell_type": "markdown", + "id": "pairwise-interpretation", "metadata": {}, - "outputs": [], "source": [ - "lambda_sparse = 0.15\n", - "biomass_fraction = 0.90\n", - "biomass_targets = {name: biomass_fraction * biomass_opt[name] for name in sample_names}\n", - "\n", - "single_sparse = {}\n", - "for name in sample_names:\n", - " d = cn.Data.from_cdict({name: condition_specs[name]})\n", - " P = MultiSampleFBA(lambda_reg=lambda_sparse).build(G, d)\n", - " P += P.expr.flow[idx[\"BIOMASS\"]] >= biomass_targets[name]\n", - " P.solve(solver=\"highs\")\n", - " single_sparse[name] = P" + "Both columns in the table describe the active reaction set for **glucose aerobic**, not the other condition in each analysis. Its medium and fixed biomass are identical in both runs, yet the selected reactions differ. The difference arises because each two-condition optimization can choose a different feasible pathway for glucose aerobic while minimizing its own reaction union. It is not a change in control biology. A post-hoc comparison cannot distinguish this optimization variability from genuine context specificity." ] }, { "cell_type": "markdown", - "id": "mc_sfba_10", + "id": "joint-analysis", "metadata": {}, "source": [ - "## Coupled multi-condition sparse FBA\n", + "## Solve all conditions jointly\n", "\n", - "Now we solve both conditions together with structured sparsity. The same biomass lower bounds are enforced, but regularization is applied jointly across conditions." + "We now place all three condition-specific flux vectors in one optimization. Glucose aerobic appears only once, alongside acetate aerobic and glucose anaerobic, and a single reaction-union penalty couples the three solutions." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_11", + "id": "solve-joint", "metadata": {}, "outputs": [], "source": [ - "P_multi_sparse = MultiSampleFBA(lambda_reg=lambda_sparse).build(G, multi_data)\n", - "for i, name in enumerate(sample_names):\n", - " P_multi_sparse += P_multi_sparse.expr.flow[idx[\"BIOMASS\"], i] >= biomass_targets[name]\n", - "P_multi_sparse.solve(solver=\"highs\")" + "joint_problem = build_sparse_problem(condition_names)\n", + "joint_fluxes = labeled_fluxes(joint_problem, condition_names)\n", + "joint_activity = joint_fluxes.abs() > activity_tolerance\n", + "\n", + "condition_summary = pd.DataFrame(\n", + " {\n", + " \"biomass target\": biomass_targets,\n", + " \"solved biomass\": joint_fluxes.loc[biomass_id],\n", + " \"active reactions\": joint_activity.sum(axis=0),\n", + " }\n", + ")\n", + "\n", + "assert (condition_summary[\"solved biomass\"] >= condition_summary[\"biomass target\"] - 1e-7).all()\n", + "condition_summary" ] }, { "cell_type": "markdown", - "id": "mc_sfba_12", + "id": "classify-reactions", "metadata": {}, "source": [ - "## Compare independent vs coupled sparse solutions" + "### Shared and context-specific reaction usage\n", + "\n", + "The activity matrix has one Boolean column per condition. Counting active columns assigns every reaction in the union to exactly one usage category." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_13", + "id": "reaction-usage", "metadata": {}, "outputs": [], "source": [ - "edge_ids = [G.get_attr_edge(i).get(\"id\") for i in range(G.num_edges)]\n", - "\n", - "\n", - "def active_ids(problem, condition=None, tol=1e-6):\n", - " flow = np.asarray(problem.expr.flow.value)\n", - " if condition is not None:\n", - " flow = flow[:, condition]\n", - " return {edge_ids[i] for i in np.where(np.abs(flow) > tol)[0]}\n", - "\n", - "\n", - "def branch_label(ab_flux, ad_flux, tol=1e-6):\n", - " ab_on = abs(float(ab_flux)) > tol\n", - " ad_on = abs(float(ad_flux)) > tol\n", - " if ab_on and not ad_on:\n", - " return \"branch_1 (A->B->C)\"\n", - " if ad_on and not ab_on:\n", - " return \"branch_2 (A->D->C)\"\n", - " if ab_on and ad_on:\n", - " return \"both\"\n", - " return \"none\"\n", - "\n", - "\n", - "rows = []\n", - "for i, name in enumerate(sample_names):\n", - " P_single = single_sparse[name]\n", - " rows.append(\n", - " {\n", - " \"setting\": f\"single sparse ({name})\",\n", - " \"selected_branch\": branch_label(\n", - " P_single.expr.flow[idx[\"R_AB\"]].value, P_single.expr.flow[idx[\"R_AD\"]].value\n", - " ),\n", - " \"biomass\": float(P_single.expr.flow[idx[\"BIOMASS\"]].value),\n", - " \"active_reactions\": len(active_ids(P_single)),\n", - " }\n", - " )\n", - " rows.append(\n", - " {\n", - " \"setting\": f\"multi sparse coupled ({name})\",\n", - " \"selected_branch\": branch_label(\n", - " P_multi_sparse.expr.flow[idx[\"R_AB\"], i].value, P_multi_sparse.expr.flow[idx[\"R_AD\"], i].value\n", - " ),\n", - " \"biomass\": float(P_multi_sparse.expr.flow[idx[\"BIOMASS\"], i].value),\n", - " \"active_reactions\": len(active_ids(P_multi_sparse, condition=i)),\n", - " }\n", - " )\n", + "active_condition_count = joint_activity.sum(axis=1)\n", + "joint_union = active_condition_count > 0\n", "\n", - "comparison = pd.DataFrame(rows)\n", - "comparison" + "reaction_usage = pd.DataFrame(\n", + " {\n", + " \"reaction\": joint_activity.index[joint_union],\n", + " \"active in\": [\n", + " \", \".join(joint_activity.columns[row])\n", + " for row in joint_activity.loc[joint_union].to_numpy()\n", + " ],\n", + " \"category\": np.select(\n", + " [\n", + " active_condition_count[joint_union] == len(condition_names),\n", + " active_condition_count[joint_union] == 1,\n", + " ],\n", + " [\"shared core\", \"context-specific\"],\n", + " default=\"partially shared\",\n", + " ),\n", + " }\n", + ")\n", + "\n", + "category_counts = reaction_usage[\"category\"].value_counts().rename(\"reactions\")\n", + "expected_categories = {\"shared core\", \"partially shared\", \"context-specific\"}\n", + "\n", + "assert set(reaction_usage[\"category\"]) == expected_categories\n", + "assert int(category_counts.sum()) == int(joint_union.sum())\n", + "category_counts.to_frame()" ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_14", + "id": "usage-patterns", "metadata": {}, "outputs": [], "source": [ - "single_union = set().union(*(active_ids(single_sparse[name]) for name in sample_names))\n", - "multi_union = set().union(*(active_ids(P_multi_sparse, condition=i) for i in range(len(sample_names))))\n", - "\n", - "summary = pd.DataFrame(\n", - " [\n", - " {\n", - " \"single_sparse_union\": len(single_union),\n", - " \"multi_sparse_union\": len(multi_union),\n", - " \"union_reduction\": len(single_union) - len(multi_union),\n", - " }\n", - " ]\n", + "usage_patterns = (\n", + " reaction_usage.groupby([\"category\", \"active in\"], sort=False)\n", + " .agg(\n", + " reactions=(\"reaction\", \"size\"),\n", + " examples=(\"reaction\", lambda values: \", \".join(values.iloc[:6])),\n", + " )\n", + " .reset_index()\n", ")\n", + "usage_patterns" + ] + }, + { + "cell_type": "markdown", + "id": "biological-interpretation", + "metadata": {}, + "source": [ + "### Biological interpretation\n", + "\n", + "The selected groups recover recognizable metabolic adaptations rather than isolated reactions:\n", + "\n", + "| Usage pattern | Representative selected reactions | Interpretation |\n", + "|---|---|---|\n", + "| Glucose anaerobic only | `ACALD`, `ALCD2x`, `ETOHt2r`, `EX_etoh_e` | A complete ethanol-fermentation route, from acetyl-CoA reduction to ethanol secretion, that supports redox balancing without respiration. |\n", + "| Acetate aerobic only | `ICL`, `MALS`, `FBP`, `PPCK`, `NADTRHD` | The glyoxylate shunt and gluconeogenesis conserve acetate carbon and produce biomass precursors; transhydrogenase contributes cofactor balancing. |\n", + "| Both aerobic conditions | `CYTBD`, `NADH16`, `EX_o2_e`, `AKGDH` | Oxygen uptake together with respiratory-chain and TCA-cycle activity shared by aerobic growth. |\n", "\n", - "# Expected in this setup: coupled sparse FBA uses fewer reactions in the union across conditions.\n", - "assert len(multi_union) < len(single_union)\n", + "These are interpretations of one sparse model solution, not evidence that the reactions are biologically exclusive to those conditions.\n", "\n", - "summary" + "> **Activity does not imply the same flux direction.** The categories above use `abs(flux) > activity_tolerance`. A reaction shared by two conditions may run in opposite directions or serve different roles. For example, the acetate transport and exchange reactions shared by acetate-aerobic and glucose-anaerobic solutions support acetate uptake in one context and acetate production or secretion in the other. Here, **shared** means that the same reaction is active—not that its direction, flux magnitude, or physiological role is identical." + ] + }, + { + "cell_type": "markdown", + "id": "network-visualization", + "metadata": {}, + "source": [ + "### Visualize the joint metabolic program\n", + "\n", + "The plot shows the complete reaction union. Shared-core reactions are gray and de-emphasized; colored reactions show how the core is adapted to carbon source and oxygen availability.\n", + "\n", + "| Active conditions | Color |\n", + "|---|---|\n", + "| All conditions | Gray |\n", + "| Both aerobic conditions | Blue |\n", + "| Both glucose conditions | Green |\n", + "| Acetate aerobic and glucose anaerobic | Purple |\n", + "| Acetate aerobic only | Orange |\n", + "| Glucose anaerobic only | Red |" ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_15", + "id": "plot-joint-network", "metadata": {}, "outputs": [], "source": [ - "pd.DataFrame(\n", - " {\n", - " \"single_sparse_union\": pd.Series(sorted(single_union)),\n", - " \"multi_sparse_union\": pd.Series(sorted(multi_union)),\n", + "reaction_groups = reaction_usage.set_index(\"reaction\")[\"active in\"].replace(\n", + " {\", \".join(condition_names): \"shared core\"}\n", + ")\n", + "\n", + "group_colors = {\n", + " \"shared core\": \"#b0b0b0\",\n", + " \"glucose aerobic, acetate aerobic\": \"#277da1\",\n", + " \"glucose aerobic, glucose anaerobic\": \"#43aa8b\",\n", + " \"acetate aerobic, glucose anaerobic\": \"#7b2cbf\",\n", + " \"acetate aerobic\": \"#f8961e\",\n", + " \"glucose anaerobic\": \"#e63946\",\n", + "}\n", + "\n", + "selected_reactions = np.flatnonzero(joint_union.to_numpy())\n", + "joint_network = G.edge_subgraph(selected_reactions)\n", + "edge_style = {}\n", + "\n", + "for displayed_index, original_index in enumerate(selected_reactions):\n", + " reaction_id = reaction_ids[original_index]\n", + " group = reaction_groups[reaction_id]\n", + " is_shared_core = group == \"shared core\"\n", + "\n", + " edge_style[displayed_index] = {\n", + " \"color\": group_colors[group],\n", + " \"penwidth\": \"1.5\" if is_shared_core else \"3\",\n", " }\n", + "\n", + "joint_network.plot(\n", + " graph_attr={\"rankdir\": \"LR\"},\n", + " node_attr={\n", + " \"fixedsize\": \"false\",\n", + " \"shape\": \"box\",\n", + " \"style\": \"rounded\",\n", + " \"margin\": \"0.05,0.03\",\n", + " },\n", + " custom_edge_attr=edge_style,\n", ")" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "mc_sfba_16", + "cell_type": "markdown", + "id": "compare-unions", "metadata": {}, - "outputs": [], "source": [ - "active_multi_c1 = np.abs(P_multi_sparse.expr.flow.value[:, 0]) > 1e-6\n", - "active_multi_c2 = np.abs(P_multi_sparse.expr.flow.value[:, 1]) > 1e-6\n", + "### Compare the selected unions\n", "\n", - "G.edge_subgraph(active_multi_c1).plot(graph_attr={\"rankdir\": \"LR\", \"center\": \"1\"})" + "The aggregate from the separate analyses includes every reaction selected in either two-condition optimization, including both alternative glucose-aerobic reaction sets. The joint analysis instead selects one reaction union while considering all three conditions simultaneously." ] }, { "cell_type": "code", "execution_count": null, - "id": "mc_sfba_17", + "id": "union-comparison", "metadata": {}, "outputs": [], "source": [ - "G.edge_subgraph(active_multi_c2).plot(graph_attr={\"rankdir\": \"LR\", \"center\": \"1\"})" + "pairwise_activity = pd.concat(\n", + " [\n", + " control_acetate_fluxes.abs() > activity_tolerance,\n", + " control_anaerobic_fluxes.abs() > activity_tolerance,\n", + " ],\n", + " axis=1,\n", + ")\n", + "pairwise_union = pairwise_activity.any(axis=1)\n", + "\n", + "union_comparison = pd.Series(\n", + " {\n", + " \"aggregate union from separate comparisons\": int(pairwise_union.sum()),\n", + " \"union from one joint analysis\": int(joint_union.sum()),\n", + " },\n", + " name=\"active reactions\",\n", + ")\n", + "\n", + "assert joint_union.sum() < pairwise_union.sum()\n", + "union_comparison.to_frame()" ] }, { "cell_type": "markdown", - "id": "mc_sfba_18", + "id": "interpretation-limitations", "metadata": {}, "source": [ - "In this toy example, independent sparse runs choose different branches, while the coupled multi-condition sparse run aligns branch usage and reduces the union of active reactions across conditions." + "## Interpretation and limitations\n", + "\n", + "The joint result provides one internally consistent reference across all three conditions. Reactions in the shared core support every selected state; partially shared and context-specific reactions describe how that core is adapted to carbon source and oxygen availability. Because a reused reaction contributes once to the union penalty, alternative pathways are selected consistently instead of independently in each comparison.\n", + "\n", + "These conditions are designed to illustrate joint reaction selection. The resulting shared and context-specific sets are properties of this model and formulation, not experimental evidence of pathway activity. Structured sparsity assumes that reusing reactions is preferable when several feasible explanations exist, and alternative joint optima may still exist.\n", + "\n", + "Continue with [gene expression integration](imat.ipynb) to learn how expression evidence selects a feasible metabolic state for one condition. The following [Multi-condition iMAT](multicondition-imat.ipynb) guide will extend that formulation across several contexts." ] } ], @@ -337,19 +574,6 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.4" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false } }, "nbformat": 4, diff --git a/docs/guide/signaling/cellnopt_ILP.ipynb b/docs/guide/signaling/cellnopt_ILP.ipynb deleted file mode 100644 index 84eb8b2c1..000000000 --- a/docs/guide/signaling/cellnopt_ILP.ipynb +++ /dev/null @@ -1,2280 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Acyclic boolean models of cell signaling (experimental)\n", - "\n", - "This guide shows the basics of modeling intracellular signaling using acyclic boolean models for multi-perturbation experiments. For this, we will use an extension of the CellNopt ILP method implemented with CORNETO.\n", - "\n", - "- Author: Attila Gabor (Heidelberg University, SaezLab)\n", - "- Reviewers: Pablo Rodriguez Mier (Heidelberg University, SaezLab)\n", - "\n", - "> NOTE: This notebook is still experimental." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - "
Installed version:v1.0.0.dev3 (latest stable: v1.0.0-alpha)
Available backends:CVXPY v1.6.0, PICOS v2.5.1
Default backend (corneto.opt):CVXPY
Installed solvers:CLARABEL, CVXOPT, GLPK, GLPK_MI, GUROBI, HIGHS, SCIP, SCIPY
Graphviz version:v0.20.3
Installed path:/Users/pablorodriguezmier/Documents/work/projects/corneto/corneto
Repository:https://github.com/saezlab/corneto
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import corneto as cn\n", - "import corneto.methods.signaling.cellnopt_ilp as cno\n", - "\n", - "cn.info()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction to a Boolean model\n", - "\n", - "As a first step we define start with a small network and corresponding datasets just to illustrate the concepts. \n", - "The prior knowledge network of 'G1' contains three nodes, EGF, TNFa and Ras. Edges show the potential activation of the RAS protein. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Definition of the prior knowledge network\n", - "G1 = cn.Graph.from_sif_tuples(\n", - " [\n", - " (\"EGF\", 1, \"AND1\"),\n", - " (\"TNFa\", 1, \"AND1\"),\n", - " (\"AND1\", 1, \"Ras\"),\n", - " (\"EGF\", 1, \"Ras\"),\n", - " (\"TNFa\", 1, \"Ras\"),\n", - " ]\n", - ")\n", - "G1.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we organize some in-silico experimental data into a format that can be used by the CNO toolbox.\n", - "\n", - "First, we define 4 conditions (exp0 ... exp3). In each condition, the activated nodes (EGF or TNFa) are set to 1 and the non-activated nodes to 0.\n", - "We also define the values of the corresponding outputs (Ras).\n", - "\n", - "Note that the output, Ras is only active (1.0) in the 4th condition, when both of the inputs are also activated. This corresponds to an AND relationship: RAS is active only if both EGF and TNFa are activated.\n", - "We expect that the optimization finds the correct subgraph of the above network, which contains only the 3 nodes with the AND gates, while individual edges from EGF and TNFa to RAS are removed. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# RAS is only active iff both EGF and TNFa are active -> we need to identify the AND gate\n", - "exp_list_G1_and = {\n", - " \"exp0\": {\"input\": {\"EGF\": 0, \"TNFa\": 0}, \"output\": {\"Ras\": 0}},\n", - " \"exp1\": {\"input\": {\"EGF\": 1, \"TNFa\": 0}, \"output\": {\"Ras\": 0}},\n", - " \"exp2\": {\"input\": {\"EGF\": 0, \"TNFa\": 1}, \"output\": {\"Ras\": 0}},\n", - " \"exp3\": {\"input\": {\"EGF\": 1, \"TNFa\": 1}, \"output\": {\"Ras\": 1}},\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G1_prep = cno.expand_graph_for_flows(G1, exp_list_G1_and)\n", - "G1_prep.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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ffv2Ure57LLLNH/+fLVr1075+fmaPn26rr76an333Xdq1qyZXC6XZx+n7vPkZ6dKT0/X+PHjK+CIAAAAUB4+31N68ODBM3727bffnlMxZREfH6/k5GTFxMSoa9euWrp0qS6++GK9/PLL5d5namqq8vPzPUtOTk4FVgwAAICz8TmUtm3bVsuXLz+tffr06ercubNP+woJCZG/v79yc3O92nNzc894z+ip6tatq6uuuko//PCDJHm282WfdrtdQUFBXgsAAACqjs+h1OFw6Pbbb1dKSop+//137d27V927d9e0adP05ptv+rSvgIAAxcbGyul0etpKSkrkdDo9E/OfTXFxsb799luFh4dLkqKjoxUWFua1T7fbrfXr15d5nwAAAKhaPt9TOmrUKP3P//yP7r33XrVr106//PKL4uLitGXLljKf3fwjh8OhAQMGqGPHjurcubNmzpypgoICz9P4ycnJatq0qdLT0yVJEyZMUJcuXdSyZUvl5eXpmWee0c8//6zBgwdLMh+wGjFihCZNmqRWrVopOjpaY8eOVUREhJKSknyuDwAAAJWvXFNCtWzZUm3atNG7774rSerTp0+5AunJbQ8ePKi0tDS5XC7FxMQoMzPT86BSdna2/Pz+e0L3119/1ZAhQ+RyudSoUSPFxsbqiy++UOvWrT19Ro0apYKCAg0dOlR5eXm69tprlZmZedok+wAAAKgebIZhGL5ssHbtWvXv31+NGzfW66+/rrVr18rhcKhXr17KyMhQo0aNKqvWKuN2uxUcHKz8/HzuLwVQqawYb8r7nWWY6Q/nCd9+8wPl58t44/M9pTfccIP69OmjdevW6YorrtDgwYP19ddfKzs7W23bti130QAAAKi9fL58v2LFCnXt2tWrrUWLFlq7dq2efvrpCisMAAAAtYfPZ0pPDaSeHfn5aezYsedcEAAAAGqfMofSG2+8Ufn5+Z71KVOmKC8vz7N++PBhr4eNAAAAgLIqcyj9+OOPvd4PP3nyZP3yyy+e9RMnTmjHjh0VWx0AAABqhTKH0lMf0vfxoX0AAADgjHy+pxQAAACoaGUOpTabTbZTJqk7dR0AAAAojzJPCWUYhgYOHCi73S5JOnbsmO6//341aNBAkrzuNwUAAAB8UeZQOmDAAK/1/v37n9YnOTn53CsCAABArVPmUPraa69VZh0AAACoxXjQCQAA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aSspKZHT6VR8fPyfbvvOO++osLBQ/fv3P+v37NmzR4cPH1Z4ePg51wwAAICKZ/nT9w6HQ3PnztXChQv1/fffKyUlRQUFBRo0aJAkKTk5WampqadtN2/ePCUlJemiiy7yaj969Kgee+wxrVu3Tj/99JOcTqduvfVWtWzZUomJiVVyTAAAAPCN5feU9unTRwcPHlRaWppcLpdiYmKUmZnpefgpOztbfn7e2XnHjh36/PPPtWLFitP25+/vry1btmjhwoXKy8tTRESEevTooYkTJzJXKQAAQDVlMwzDsLqI6sbtdis4OFj5+fncXwqgUlkx3jDGoarZbFZXgIria2r0Zbwp8+X748ePa9SoUWrZsqU6d+582uT2ubm58vf3961SAAAAQD6E0qefflqLFi3S/fffrx49esjhcOjvf/+7Vx9OugIAAKA8ynxP6RtvvKFXX31VN910kyRp4MCB6tWrlwYNGuQ5a2rj/DwAAADKocxnSvfu3as2bdp41lu2bKnVq1friy++0L333nvGtyUBAAAAZ1PmUBoWFqZdu3Z5tTVt2lSrVq3Sl19+qYEDB1Z0bQAAAKglyhxKb7jhBr355puntUdEROiTTz7R7t27K7QwAAAA1B5lvqd07Nix2r59e6mfNW3aVGvWrNHKlSsrrDAAAADUHmUOpZdccokuueSSM34eERGhAQMGVEhRAAAAqF0sf80oAAAAQCgFAACA5QilAAAAsByhFAAAAJYjlAIAAMByFRZKBwwYoBtuuKGidgcAAIBapMxTQp1N06ZN5efHiVcAAAD4rsJC6eTJkytqVwAAAKhlzunUpmEYMgyjomoBAABALVWuUDpv3jy1adNGgYGBCgwMVJs2bfTqq69WdG0AAACoJXy+fJ+WlqYZM2Zo+PDhio+PlyRlZWVp5MiRys7O1oQJEyq8SAAAANRsPofSOXPmaO7cuerXr5+n7ZZbblG7du00fPhwQikAAAB85vPl++PHj6tjx46ntcfGxurEiRMVUhQAAABqF59D6b333qs5c+ac1v7KK6/onnvuKVcRs2fPVlRUlAIDAxUXF6cNGzacse+CBQtks9m8lsDAQK8+hmEoLS1N4eHhqlevnhISErRz585y1QYAAIDKV64poebNm6cVK1aoS5cukqT169crOztbycnJcjgcnn4zZsw4676WLFkih8OhjIwMxcXF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nj3m0/PPP2+GUYfDfLoeAACUi8+hdNSoUcrKytJnn32mdu3aqV27drLb7dqyZYv+9re/+bSvgIAAxcbGyul0etpKSkrkdDo9b4s6m+LiYn377bcKDw+XJEVHRyssLMxrn263W+vXry/zPgGPo0elZ56RoqPN4Ll/v/SXv0gvvWResn/oIXPeUQAAcE7KNSVUy5Yt1aZNG7377ruSpD59+pT7LKTD4dCAAQPUsWNHde7cWTNnzlRBQYEGDRokSUpOTlbTpk2Vnp4uSZowYYK6dOmili1bKi8vT88884x+/vlnDR48WJL51P+IESM0adIktWrVyjMlVEREhJKSkspVI2oht9u8TD9jhnT4sNkWHS2NGSMlJ0sBAdbWBwBADeNzKF27dq369++vxo0ba8uWLVq7dq2GDx+uDz/8UBkZGWrUqJFP++vTp48OHjyotLQ0uVwuxcTEKDMz0/OgUnZ2tvz8/ntC99dff9WQIUPkcrnUqFEjxcbG6osvvlDr1q09fUaNGqWCggINHTpUeXl5uvbaa5WZmckcpTi7vDzphRfMS/Unpx1r1cp8cOnuu8331AMAgArn8zyldrtdI0eO1MSJE1X3/35B79q1S/3791dOTo7nKfjzmRXzBsJihw9LM2eagdTtNtsuv1x68kmpTx+pTrkuKgBnxTylsALzlKK8qtU8pStWrFDXrl292lq0aKG1a9fq6aef9nV3gLUOHjSfnJ8927x/VJLatJHGjpVuv13y97e2PgAAagmfQ+mpgfQkPz8/jR079pwLAqqEyyVNny7NmWM+WS9JMTFSWpp0662Sn8/PAAIAgHNQ5t+8N954o/Lz8z3rU6ZM8XrV5+HDh73u6wSqpb17pYcfNh9aevZZM5B26iT961/Spk3S3/5GIAUAwAJl/u378ccfe70ffvLkyfrll1886ydOnNCOHTsqtjqgomRnSw88YM4z+sIL5iT48fHSRx9J69dLN9/MTU8AAFiozJfvT30eysfnowBr/PijlJ4uLVxovqdekv76V/My/Q03EEQBAKgmeKQYNdPOndLkydI//iEVF5tt3bubDzCd4b5oAABgnTKHUpvNJtspZ5VOXQcs9/330tNPS2+9JZWUmG09e5ph9Oqrra0NAACckU+X7wcOHCi73S5JOnbsmO6//341+L/3ff/xflOgyn37rTRpkvTOO/+dRO2m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eo6WLFkih8OhcePGadOmTWrfvr0SExN14MABq0vz2Zo1a/Tggw9q3bp1WrlypY4fP64ePXqooKDA6tLO2ZdffqmXX35Z7dq1s7qUcvv11191zTXXqG7duvroo4+0bds2Pfvss2rUqJHVpfls6tSpmjNnjmbNmqXvv/9eU6dO1bRp0/Tiiy9aXVqZFBQUqH379po9e3apn0+bNk0vvPCCMjIytH79ejVo0ECJiYk6duxYFVeKc3Xw4EGVlJTIZrNZXQpQ8xk4J507dzYefPBBz3pxcbERERFhpKenW1hVxThw4IAhyVizZo3VpZyTI0eOGK1atTJWrlxpdO3a1Xj44YetLqlcHn/8cePaa6+1uowK0bt3b+N///d/vdpuu+0245577rGoovKTZLz33nue9ZKSEiMsLMx45plnPG15eXmG3W433nrrLQsqRHl9/fXXxhVXXGF8+umnVpcC1AqcKT0HRUVF2rhxoxISEjxtfn5+SkhIUFZWloWVVYz8/HxJUuPGjS2u5Nw8+OCD6t27t9e/p/PRv/71L3Xs2FF33nmnmjRpoquuukpz5861uqxyufrqq+V0OvWf//xHkvTNN9/o888/V69evSyu7Nzt3r1bLpfL6+ctODhYcXFxNWJcqC2++eYbdenSRbfccouuu+46q8vBecr4v9s+vv32Wy1ZskT//ve/tXv3bourqr6qxWtGz1eHDh1ScXGxQkNDvdpDQ0O1fft2i6qqGCUlJRoxYoSuueYatWnTxupyym3x4sXatGmTvvzyS6tLOWc//vij5syZI4fDoTFjxujLL7/UQw89pICAAA0YMMDq8nwyevRoud1uXX755fL391dxcbGefvpp3XPPPVaXds5cLpcklTounPwM1duWLVt0zTXX6NFHH9WkSZM87ceOHVNgYKCFleF8UlxcLH9/fy1dulQjRoxQcHCwGjRooMLCQr366quKjY21usRqhzOlKNWDDz6orVu3avHixVaXUm45OTl6+OGH9cYbb9SIXyQlJSXq0KGDJk+erKuuukpDhw7VkCFDlJGRYXVpPnv77bf1xhtv6M0339SmTZu0cOFCTZ8+XQsXLrS6NNRy+/fvV0xMjO666y6vQDp+/Hg99dRTOnHihIXV4XzgdrslSf7+/lq1apWGDBmiMWPG6Ntvv9XYsWO1detW3XjjjVq7dq3FlVY/hNJzEBISIn9/f+Xm5nq15+bmKiwszKKqzt2wYcP0wQcfaNWqVWrWrJnV5ZTbxo0bdeDAAXXo0EF16tRRnTp1tGbNGr3wwguqU6eOiouLrS7RJ+Hh4WrdurVX2xVXXKHs7GyLKiq/xx57TKNHj1bfvn3Vtm1b3XvvvRo5cqTS09OtLu2cnfxvv6aNC7VF/fr1dfXVVysrK0tbt26VZD649uyzz+qvf/2r6tThAiPObNOmTWrcuLG2bdum3377Te+++64eeOAB3X///dq3b58eeOAB3X777erSpYuSkpK0ceNGq0uuVgil5yAgIECxsbFyOp2etpKSEjmdTsXHx1tYWfkYhqFhw4bpvffe0yeffKLo6GirSzon3bt317fffqvNmzd7lo4dO+qee+7R5s2b5e/vb3WJPrnmmmtOm6LrP//5jy655BKLKiq/3377TX5+3sOPv7+/SkpKLKqo4kRHRyssLMxrXHC73Vq/fv15OS7UNsHBwfroo48UHh6u22+/XSNGjNAzzzyjpUuX6sYbb7S6PFRj33zzjbp166YRI0aodevWql+/vu6991716NFDeXl5uuWWW9SzZ08tXrxYgwYN0uHDh9W5c2fuNf8jq5+0Ot8tXrzYsNvtxoIFC4xt27YZQ4cONS688ELD5XJZXZrPUlJSjODgYGP16tXG/v37Pctvv/1mdWkV5nx++n7Dhg1GnTp1jKefftrYuXOn8cYbbxj169c3Xn/9datL89mAAQOMpk2bGh988IGxe/duY+nSpUZISIgxatQoq0srkyNHjhhff/218fXXXxuSjBkzZhhff/218fPPPxuGYRhTpkwxLrzwQuP99983tmzZYtx6661GdHS08fvvv1tcOUpz8OBBY926dcamTZs8bUeOHDFuvPFGw2azGRkZGRZWh/PBli1bjHr16hljx471aj/5+3PFihVGfHy8sXv3bsMwDGPdunVG7969jYceesjYvn17VZdbbRFKK8CLL75o/OUvfzECAgKMzp07G+vWrbO6pHKRVOry2muvWV1ahTmfQ6lhGMa///1vo02bNobdbjcuv/xy45VXXrG6pHJxu93Gww8/bPzlL38xAgMDjebNmxtPPPGEUVhYaHVpZbJq1apS/1sZMGCAYRjmtFBjx441QkNDDbvdbnTv3t3YsWOHtUWjVNu2bTO6du1q3HTTTUZycrLXZ/n5+cb//M//GM2bNze++eYbiypEdZednW1cfPHFxq233urVPmPGDGPkyJFGcXGxsXjxYsPPz8/47rvvDMMwjDFjxhj9+vUzjhw5YkHF1ZfNMHhNBQCg9vn22291/fXX64EHHtCQIUP0l7/8RZK0efNmhYWFKSwsTEePHtXNN9+sn376Sf/617/Utm1bi6tGdbNlyxYlJyerefPmGjx4sG688UY988wzeuqpp/Tvf/9bN9xwg7Kzs/XAAw9o7dq1at++vb766itlZWXx83QKQikAoNbZt2+fevTooR49emjGjBme9qlTpyo1NVWjR4/WI488oosuukhHjx5VUlKSvvzyS2VlZZ32wCFqJ7fbrYCAAAUGBmrDhg0aNWqULrroIl1wwQX68MMPtWTJEt1www2e/l9//bU++eQT/fLLL0pOTtZll11mYfXVE48RAgBqnY0bNyowMFB///vfVVJSIj8/P02fPl0TJ07Uww8/rGnTpslms8nhcOiiiy7Se++9p3vuuUcBAQFWl45qYP/+/UpOTtatt96q++67T507d9bUqVP1+OOPKzMzU6NHj/YE0hMnTqhOnTq66qqrdNVVV8kwDF5bewaEUgBArbNmzRr98ssvnrNVJ06cUFhYmJYtW6aEhAR17txZ99xzj4qKijR+/Hg1bNhQ77//PmECksw3Hfr7++vNN99UYGCg7rnnHsXFxem5556Tw+HQl19+qeXLl6t3796qU6eO53988Of4JwQAqBV27tzpmaqrYcOG8vPz08GDB1VcXKw6deronnvuUUJCggzDUL9+/XTfffdp3bp1nunjCKSQpOPHj8tut+v9999Xs2bN9Morr+iNN97Q77//rquuukpTp07VkSNH9NJLL+mjjz6SJK9Ays/RmRFKAQA13ubNm9WhQwfPXL/x8fH68ccftWzZslLnLD5+/Lj8/f0VFxfHGS5Iko4cOSJJqlu3riTJbrdr0aJF+stf/qKXX37ZE0xPXsovKirS5MmTtWLFCivLPq/wXxoAoEb75ptvdM0112jYsGF64IEHJEnXX3+9Bg4cqL///e966623JP33DFZJSYnGjRunf/3rXxo6dKgnhKD2+v7779WsWTP17dtXY8aM0Y8//qh9+/YpMDBQb7zxhi6//HK99NJLev311/Xbb7+pc+fOeuqpp3ThhRfqiiuusLr88wZP3wMAaqwtW7YoPj5eI0aM0NNPP+1pX7VqlVwul5YsWaJ///vfuv/++3XNNdfol19+0bp16/Thhx/K6XTqqquusrB6VAcnTpzQokWLNHjwYIWFhSk6Olo//vijQkJC1LNnT/Xs2VOdOnXSwIED9fvvv+uOO+7Q3XffrXr16unYsWMKDAy0+hDOGzzoBACokXJyctS9e3fddNNNXoF0woQJeu211+R0OhUbG6sOHTro+eef1xtvvKHw8HDFxMTo888/Z+on6LvvvtPSpUs1YsQIHThwQE888YRefPFFhYSEaNu2bVq0aJEWL16ssLAwhYaGas2aNdqxY4fn4Se73W71IZxXuHwPAKiRiouLFR0drWPHjmnt2rWSpClTpujFF1/U7Nmz1bx5c1166aVKS0vTd999p82bN2vt2rVasGABgRT65ptv1LZtW9WpU0cNGzbU6NGj5XA4dO+992rfvn1KSUnRqlWr9M033+iuu+7SlVdeqYCAAP3222/q0qWLJB5q8hWX7wEANdbOnTv10EMPKSAgQKGhoVq2bJlef/119ejRw2u+yG3bthFE4bFt2zZ17NhRo0aN0lNPPeX12eOPP67nnntO8+fPV//+/b0++/HHH9WwYUNdfPHFVVhtzUEoBQDUaP/5z380bNgwff7555o4caIeeeQRnfzVZ7PZNHbsWC1YsEDffvutgoODObtVy23dulXdunXTxRdfrG3btkkyZ2P44wNvo0eP1owZM7Rw4UL169fPqlJrHC7fAwBqtEsvvVRz5szRddddJ6fTqc8++0w2m002m01paWmaPn26li1bpgsvvJBAWst98803iouLU5s2bZSfn6+HH35YkjkNVHFxsafflClT5HA4NGTIEL322mtWlVvjcKYUAFArnLyUbxiG0tPTtXLlSo0bN06ff/65YmNjrS4PFvvqq6909dVX64knntCTTz6pefPm6YknntDdd9+t559/XpJ5n/If57UdNmyY3nnnHe3cuVNBQUFWlV5jEEoBALXGzp075XA4tGHDBv3666/KysoikEKS9Omnn+rdd9/1BND8/HwtWbLkrMH0wIEDatKkiSU11zSEUgBArbJjxw6NGjVKkydP1pVXXml1OaiGTj4E53a7tXjx4tOC6YkTJ1SnDrNqVjRCKQCg1jn1wRXgTP4YTO+9917NmDHD6pJqLGI+AKDWIZCirIKCgtS3b1/5+flp6NChstvtSk9Pt7qsGolQCgAA8CeCgoJ05513qm7duoqPj7e6nBqLy/cAAABl8McXLqDiMU8pAABAGRBIKxehFAAAAJYjlAIAAMByhFIAAABYjlAKAAAAyxFKAQAAYDlCKQAAACxHKAUAAIDlCKUAAACwHKEUAAAAliOUAgAAwHKEUgAAAFju/wPudo4GOjvDrAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_data(exp_list_G1_and)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Set parameter Username\n", - "Set parameter LicenseID to value 2593994\n", - "Academic license - for non-commercial use only - expires 2025-12-02\n" - ] - } - ], - "source": [ - "P = cno.cellnoptILP(G1_prep, exp_list_G1_and, verbose=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAqUAAAIYCAYAAABUjJWZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmc0lEQVR4nO3df3zN9f//8fvZtDNkC3vbMZafpbxtVmNrSpR9TMk7n37ROw3fqChvOpWsdyaphuStIiQ/5lsi77fU+63Ed2+jNJbfFEvCVpxh7BwmG2ev7x/ezruTYWe2vWa7XS+X1yWv5+v5ep7HS6fnefT68XhZDMMwBAAAAJjIz+wAAAAAAJJSAAAAmI6kFAAAAKYjKQUAAIDpSEoBAABgOpJSAAAAmI6kFAAAAKYjKQUAAIDpSEoBAABgOpJSAAAAmI6kFLiAefPmyWKxeJZatWqpSZMmGjBggH755RezwwMAn/x+TgsMDNT111+vp59+Wrm5ueX2Oe+++67mzZtXbuOh5qhldgBAVffKK6+oRYsWOnXqlNatW6d58+bp66+/1o4dOxQYGGh2eADgk9/OaV9//bWmT5+uzz//XDt27FCdOnUue/x3331XISEhGjBgwOUHixqFpBS4hLvuuksdOnSQJA0aNEghISGaMGGCPvvsMz300EMmRwcAvvn9nNawYUNNnjxZn376qR5++OEyj3vy5MlySWpRc3H5HvBR586dJUl79uyRJBUVFSk5OVnR0dEKDg5W3bp11blzZ61ateq8fRcuXKjo6GjVq1dPQUFBioiI0FtvvVWp8QPAb915552SpL1790qSPvjgA0VHR6t27dpq0KCB+vbtq5ycHK99unbtqnbt2mnjxo26/fbbVadOHb344otq3ry5vvvuO61evdpzm0DXrl0r+5BwheJMKeCjffv2SZLq168vSXK5XHr//ff18MMPa/DgwTp+/Lhmz56thIQEZWZmKioqSpK0cuVKPfzww+rWrZsmTJggSdq5c6fWrl2r4cOHm3EoAOD5H+yGDRvqtdde0+jRo/XQQw9p0KBBOnz4sN555x3dfvvt2rx5s6655hrPfnl5ebrrrrvUt29f9evXT6GhoeratauGDRumq6++Wn/9618lSaGhoWYcFq5AJKXAJTidTh05ckSnTp3S+vXrNXbsWFmtVt1zzz2Szian+/btU0BAgGefwYMH64YbbtA777yj2bNnS5KWLVumoKAgffnll/L39zflWADgt3Pa2rVr9corr6h27drq0aOHWrVqpVdffVUvvviip/99992nm266Se+++65Xu8Ph0IwZM/TEE094jf/SSy8pJCRE/fr1q7RjQvVAUgpcQnx8vNd68+bN9cEHH6hp06aSJH9/f0+SWVxcrPz8fBUXF6tDhw7atGmTZ79rrrlGBQUFWrlypXr06FF5BwAAv/H7Oa1Zs2b68MMPtWTJEhUXF+uhhx7SkSNHPNttNpuuu+46rVq1yisptVqtGjhwYKXFjeqPpBS4hGnTpun666+X0+nUnDlztGbNGlmtVq8+qampevPNN7Vr1y6dPn3a096iRQvPn4cOHaqPP/5Yd911l5o0aaLu3bvroYceIkEFUKnOzWm1atVSaGio2rRpIz8/P3366acyDEPXXXddiftdddVVXutNmjTxukIEXC6SUuASYmJiPE+q9u7dW7fddpv+/Oc/KysrS1dffbU++OADDRgwQL1799bzzz+vRo0ayd/fXykpKZ57tSSpUaNG2rJli7788kt98cUX+uKLLzR37lwlJiYqNTXVrMMDUMP8dk77reLiYlksFn3xxRcl3mJ09dVXe63Xrl27wmJEzURSCvjgXLJ5xx13aOrUqRo1apT+/ve/q2XLllqyZIksFoun75gxY87bPyAgQL169VKvXr1UXFysoUOHaubMmRo9erRat25dmYcCAF5atWolwzDUokULXX/99WUe57fzIOALSkIBPuratatiYmI0ZcoUnTp1ynNGwTAMT5/169crIyPDa7+8vDyvdT8/P0VGRkqSCgsLKzhqALi4++67T/7+/ho7dqzXfCadnd9+P4ddSN26dZWfn39e++nTp7Vr1y4dPHiwPMJFNcSZUqAMnn/+eT344IOaN2+e7rnnHi1ZskT/+7//q549e2rv3r2aMWOG2rZtqxMnTnj2GTRokI4ePao777xTTZs21f79+/XOO+8oKipKN954o4lHAwDyPHmflJSkffv2qXfv3qpXr5727t2rTz75RI8//riee+65S44THR2t6dOn69VXX1Xr1q3VqFEj3Xnnnfrll1904403qn///ryGFCUiKQXK4L777lOrVq00adIkZWVlyeFwaObMmfryyy/Vtm1bffDBB1q8eLHS09M9+/Tr10/vvfee3n33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r7zyik/jLVq0SHa7XTNmzFBsbKymTJmihIQEZWVlqVGjRiXuExQUpKysLM+6xWLx2j5x4kS9/fbbSk1NVYsWLTR69GglJCTo+++/Py+BBQAAgPl8PlN6+vRpdejQ4bz26OhonTlzxucAJk+erMGDB2vgwIFq27atZsyYoTp16mjOnDkX3Mdischms3mW0NBQzzbDMDRlyhS99NJLuvfeexUZGan58+frwIEDWrp0qc/xAQAAoOL5nJQ++uijmj59+nnt7733nh555BGfxioqKtLGjRsVHx//34D8/BQfH6+MjIwL7nfixAk1a9ZM4eHhuvfee/Xdd995tu3du1cOh8NrzODgYMXGxl5wzMLCQrlcLq8FAAAAladMJaFmz56tFStW6JZbbpEkrV+/XtnZ2UpMTJTdbvf0mzx58kXHOXLkiNxut9eZTkkKDQ3Vrl27StynTZs2mjNnjiIjI+V0OjVp0iR16tRJ3333nZo2bSqHw+EZ4/djntv2eykpKRo7duzFDxoAAAAVxuekdMeOHbr55pslSXv27JEkhYSEKCQkRDt27PD0+/19nuUlLi5OcXFxnvVOnTrpxhtv1MyZMzVu3LgyjZmUlOSVTLtcLoWHh192rAAAACgdn5PSVatWlduHh4SEyN/fX7m5uV7tubm5stlspRrjqquu0k033aQff/xRkjz75ebmqnHjxl5jRkVFlTiG1WqV1WotwxEAAACgPPh8T+nhw4cvuG379u0+jRUQEKDo6GilpaV52oqLi5WWluZ1NvRi3G63tm/f7klAW7RoIZvN5jWmy+XS+vXrSz0mAAAAKpfPSWlERISWLVt2XvukSZMUExPjcwB2u12zZs1Samqqdu7cqSFDhqigoEADBw6UJCUmJiopKcnT/5VXXtGKFSv0008/adOmTerXr5/279+vQYMGSTp728CIESP06quv6rPPPtP27duVmJiosLAw9e7d2+f4AAAAUPF8vnxvt9t1//33a+DAgZo8ebKOHj2qxMREbd++XQsWLPA5gD59+ujw4cNKTk6Ww+FQVFSUli9f7nlQKTs7W35+/82djx07psGDB8vhcKh+/fqKjo7WN998o7Zt23r6jBw5UgUFBXr88ceVn5+v2267TcuXL6dGKQAAQBVlMcrw8vrNmzfr0UcfVWFhoY4eParY2FjNmTOn1PeBVnUul0vBwcFyOp0KCgoyOxwA1ZgZ8w1zHCpbBT37DBP4mjX6Mt/4fPleklq3bq127dpp3759crlc6tOnT7VJSAEAAFD5fE5K165dq8jISO3evVvbtm3T9OnTNWzYMPXp00fHjh2riBgBAABQzfmclN55553q06eP1q1bpxtvvFGDBg3S5s2blZ2drYiIiIqIEQAAANWczw86rVixQl26dPFqa9WqldauXavXXnut3AIDAABAzeHzmdLfJ6Segfz8NHr06MsOCAAAADVPqZPSu+++W06n07M+fvx45efne9bz8vK8yjIBAAAApVXqpPTLL79UYWGhZ/3111/X0aNHPetnzpxRVlZW+UYHAACAGqHUSenvy5mWobwpAAAAUKIy1SkFAAAAylOpk1KLxSLL717J8Pt1AAAAoCxKXRLKMAwNGDBAVqtVknTq1Ck9+eSTqlu3riR53W8KAAAA+KLUSWn//v291vv163den8TExMuPCAAAADVOqZPSuXPnVmQcAAAAqMF40AkAAACmIykFAACA6UhKAQAAYDqSUgAAAJiOpBQAAACmIykFAACA6UhKAQAAYLoqkZROmzZNzZs3V2BgoGJjY5WZmXnBvrNmzVLnzp1Vv3591a9fX/Hx8ef1HzBggOe1qOeWHj16VPRhAAAAoIxMT0oXLVoku92uMWPGaNOmTWrfvr0SEhJ06NChEvunp6fr4Ycf1qpVq5SRkaHw8HB1795dv/zyi1e/Hj166ODBg57lo48+qozDAQAAQBlYDMMwzAwgNjZWHTt21NSpUyVJxcXFCg8P17BhwzRq1KhL7u92u1W/fn1NnTrV85rTAQMGKD8/X0uXLi1VDIWFhSosLPSsu1wuhYeHy+l0KigoyPeDAoBScrlcCg4OrtT5xozPRM1msZgdAcqLr1mjL/ONqWdKi4qKtHHjRsXHx3va/Pz8FB8fr4yMjFKNcfLkSZ0+fVoNGjTwak9PT1ejRo3Upk0bDRkyRHl5eRccIyUlRcHBwZ4lPDy8bAcEAACAMjE1KT1y5IjcbrdCQ0O92kNDQ+VwOEo1xgsvvKCwsDCvxLZHjx6aP3++0tLSNGHCBK1evVp33XWX3G53iWMkJSXJ6XR6lpycnLIfFAAAAHxWy+wALsf48eO1cOFCpaenKzAw0NPet29fz58jIiIUGRmpVq1aKT09Xd26dTtvHKvVKqvVWikxAwAA4HymnikNCQmRv7+/cnNzvdpzc3Nls9kuuu+kSZM0fvx4rVixQpGRkRft27JlS4WEhOjHH3+87JgBAABQ/kxNSgMCAhQdHa20tDRPW3FxsdLS0hQXF3fB/SZOnKhx48Zp+fLl6tChwyU/5+eff1ZeXp4aN25cLnEDAACgfJleEsput2vWrFlKTU3Vzp07NWTIEBUUFGjgwIGSpMTERCUlJXn6T5gwQaNHj9acOXPUvHlzORwOORwOnThxQpJ04sQJPf/881q3bp327duntLQ03XvvvWrdurUSEhJMOUYAAABcnOn3lPbp00eHDx9WcnKyHA6HoqKitHz5cs/DT9nZ2fLz+2/uPH36dBUVFemBBx7wGmfMmDF6+eWX5e/vr23btik1NVX5+fkKCwtT9+7dNW7cOO4bBQAAqKJMr1NaFVHDD0BloU4pagLqlFYf1bZOKQAAACCRlAIAAKAKICkFAACA6UhKAQAAYDqSUgAAAJiOpBQAAACmIykFAACA6UhKAQAAYDqSUgAAAJiOpBQAAACmIykFAACA6UhKAQAAYDqSUgAAAJiOpBQAAACmIykFAACA6UhKAQAAYDqSUgAAAJiOpBQAAACmq2V2ANWBu8it7e9+pZN7DqpOq8aKGNpZ/gH+ZocFoJIxFwBA2ZGUXqZ1I5fo2snDFeX+2dN24Lmmyra/pVsm3mdiZAAqE3MBAFyeKnH5ftq0aWrevLkCAwMVGxurzMzMi/ZfvHixbrjhBgUGBioiIkKff/6513bDMJScnKzGjRurdu3aio+P1+7du8s97nUjlyjmjQdk+82PkCTZ3L8o5o0HtG7kknL/TABVD3MBAFw+i2EYhpkBLFq0SImJiZoxY4ZiY2M1ZcoULV68WFlZWWrUqNF5/b/55hvdfvvtSklJ0T333KMFCxZowoQJ2rRpk9q1aydJmjBhglJSUpSamqoWLVpo9OjR2r59u77//nsFBgZeMiaXy6Xg4GA5nU4FBQWV2Mdd5FZuneayuX8uMbMvlkUOvya6ev93XL4DqjF3kVsFzdoqtPiXC84FB/2bynZyb4lzQWnmm/JmxmeiZrNYzI4A5cXXrNGn+cYwWUxMjPHUU0951t1utxEWFmakpKSU2P+hhx4yevbs6dUWGxtrPPHEE4ZhGEZxcbFhs9mMN954w7M9Pz/fsFqtxkcffVTimKdOnTKcTqdnycnJMSQZTqfzgnFv/tsqwzj774aFhYXlksvmv60qcS5xOp3Gpeab8mbGZ6JmqwL/CbKU0+IrX+YbUy/fFxUVaePGjYqPj/e0+fn5KT4+XhkZGSXuk5GR4dVfkhISEjz99+7dK4fD4dUnODhYsbGxFxwzJSVFwcHBniU8PPySsZ/cc/CSfQDgHOYMALg4Ux90OnLkiNxut0JDQ73aQ0NDtWvXrhL3cTgcJfZ3OBye7efaLtTn95KSkmS32z3rLpfrkolpnVaNL7r9nG9f+Vxtn7i9VH0BXHm+n7lGHZPvvmS/0s4ZAFBT8fS9JKvVKqvV6tM+EUM768BzTWVz/yI/GedtP3cf2c0vdOeeUqAau/mF7jow9tJzQcTQziZEBwBXDlMv34eEhMjf31+5uble7bm5ubLZbCXuY7PZLtr/3D99GbMs/AP8lW1/S9LZH53fOreeY59CQgpUc8wFAFA+TE1KAwICFB0drbS0NE9bcXGx0tLSFBcXV+I+cXFxXv0laeXKlZ7+LVq0kM1m8+rjcrm0fv36C45ZVrdMvE+Zz/9dDv8mXu0H/Zsq8/m/U5sQqCGYCwDg8pl++d5ut6t///7q0KGDYmJiNGXKFBUUFGjgwIGSpMTERDVp0kQpKSmSpOHDh6tLly5688031bNnTy1cuFAbNmzQe++9J0myWCwaMWKEXn31VV133XWeklBhYWHq3bt3qWIyjLOX4Fwu1yX7tn0pXu6R2/T1+9/o170O1W5h0x8HdVLbAP9S7Q+geijrXHBu27l5pzL4MscB5cHpNDsClBdfpw2f5jjfH+4vf++8845x7bXXGgEBAUZMTIyxbt06z7YuXboY/fv39+r/8ccfG9dff70REBBg/PGPfzSWLVvmtb24uNgYPXq0ERoaalitVqNbt25GVlZWqeM5VxKKhYWFpbKWnJycy5pHfcEcx8LCUtlLaeY404vnV0XFxcU6cOCA6tWrJwsVfwFUIMMwdPz4cYWFhcnPr3LuqGKOA1BZfJnjSEoBAABgOlMfdAIAAAAkklIAAABUASSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdCSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdLXMDqAq4r3QACqLL++FLi/McQAqiy9zHElpCQ4cOKDw8HCzwwBQg+Tk5Khp06aV8lnMcQAqW2nmOJLSEtSrV0/S2b/AoKAgk6MBUJ25XC6Fh4d75p3KwBwHoLL4MseRlJbg3OWsoKAgJmwAlaIyL6MzxwGobKWZ43jQCQAAAKYjKQUAAIDpSEoBAABgOpJSAAAAmI6kFAAAAKbj6XsAKCfuIre2v/uVTu45qDqtGitiaGf5B/ibHRYAXBFMPVO6Zs0a9erVS2FhYbJYLFq6dOkl90lPT9fNN98sq9Wq1q1ba968eef1mTZtmpo3b67AwEDFxsYqMzOz/IMHgN9YN3KJcus0V9Qzd6jT1D8r6pk7lFunudaNXGJ2aABwRTA1KS0oKFD79u01bdq0UvXfu3evevbsqTvuuENbtmzRiBEjNGjQIH355ZeePosWLZLdbteYMWO0adMmtW/fXgkJCTp06FBFHQaAGm7dyCWKeeMB2dw/e7Xb3L8o5o0HSEwBoBQshmEYZgchnS2q+sknn6h3794X7PPCCy9o2bJl2rFjh6etb9++ys/P1/LlyyVJsbGx6tixo6ZOnSrp7Duew8PDNWzYMI0aNapUsbhcLgUHB8vpdFJYGsBFuYvcyq3TXDb3zyX+X36xLDro31S2k3tLvJRvxnzDHAegsvgy31xRDzplZGQoPj7eqy0hIUEZGRmSpKKiIm3cuNGrj5+fn+Lj4z19SlJYWCiXy+W1AEBpbH/3K4VdICGVJD8ZauLO0fZ3v6rUuADgSnNFPejkcDgUGhrq1RYaGiqXy6Vff/1Vx44dk9vtLrHPrl27LjhuSkqKxo4dWyExA6i+tk79SoFJw0vV9+SegxUcTcWrxDehooJV9jVSvjvVR0V+d66oM6UVJSkpSU6n07Pk5OSYHRKAKsooNvTty8u0Leg2tR92u244ta1U+9Vp1biCIwOAK9sVdabUZrMpNzfXqy03N1dBQUGqXbu2/P395e/vX2Ifm812wXGtVqusVmuFxAygejhz6owyn1+shu+PV8f/JKKFCtC6Nv11w+5/6g/FufLT+acQzt1TGjG0c2WHDABXlCvqTGlcXJzS0tK82lauXKm4uDhJUkBAgKKjo736FBcXKy0tzdMHAHxR6CrUmn7v6Zd6N6jT1D+rzaltOq6rld7hOR3buFdddr2nvc+erSBSLO9rlOfWc+xTqFcKAJdgalJ64sQJbdmyRVu2bJF0tuTTli1blJ2dLensZfXExERP/yeffFI//fSTRo4cqV27dundd9/Vxx9/rGeeecbTx263a9asWUpNTdXOnTs1ZMgQFRQUaODAgZV6bACubMcPHFd6rzd1rH4L3f7hE2p2Zo/yLA2VfucrOvPjfnX99g3Zbg6TJN0y8T5lPv93OfybeI1x0L+pMp//u26ZeJ8ZhwAAVxbDRKtWrTIknbf079/fMAzD6N+/v9GlS5fz9omKijICAgKMli1bGnPnzj1v3Hfeece49tprjYCAACMmJsZYt26dT3E5nU5DkuF0Ost4ZACuVHk/HDFW3Z5sHLXUN4yz9/QbB/yaGOm9/2acyD1x0X3PFJ4xNv9tlbH26QXG5r+tMs4Unrnk55kx35T1M//z18FSDZbKZvbxspj33fFlvqkydUqrEmr4ATXPwQ2/KOuJN9Vh03u6WgWSpL1XXaefHxml2Hf6KeDqgAr53CupTilPUFcflf3Lz3en+vD1u+PLfHNFPegEAOVt38rdynl6gmJ/mK+uOi1J2lU7SseefFEx4+9TC+4FBYBKQVIKoEbKWrRFec+nKDbn72quYknSluDbdea5JEW/mCCLH6d2AKAykZQCqFG2Tv1KRa+kqOPhLzxtmY3uUe1XkhT1RCcTIwOAmo2kFEC1ZxQb2vDK57JOTlH742slSW75aV2zPvrDpFGKeSDS5AgBACSlAKotd5Fb659brIazxqvjqa2Szha8X3/jQDWb+rxuvbOVyRECAM4hKQVQ7RS6CrV+aKqaLZqoTmf2SJJOqK42dBiiG2Y+o9v/U18UAFB1kJQCqDZOOE5ow+CZavP5ZN1efECSlGdpqO1d/6L2s55W11YNTI4QAHAhJKUArnhHd+dp2+B31H7N2+pqHJMkHfRroh/+9Jw6zBysro3qmhwhAOBSSEoBXLF+W/C+628L3v/5BcW83U9dgqwmRwgAKC2SUgBXnH0rdyt72ETdkpXqVfD+6BNJip1wPwXvAeAKRFIK4IpBwXsAqL5ISgFUeSUXvO+pwJeTFDXkVhMjAwCUF5JSAFWSUWxow7gvZH3zdQreA0ANQFIKoEq5YMH7GwacLXjfrbXJEQIAKgJJKYAqodBVqPVPz9e1Cyeq0+kfJf2n4H30k7rhPTsF7wGgmiMpBWCqkgreH7U00Lauwyl4DwA1CEkpAFOcK3gfueYddTWOSjpb8D6r17PqMGOwutquNjlCAEBlIikFUKkObvhFWU9OVoeNM0sseN+VgvcAUCORlAKoFBS8BwBcDEkpgAp1tuD9eMXmLPYUvN8a1Fmnn3+RgvcAAA8/swOQpGnTpql58+YKDAxUbGysMjMzL9i3a9euslgs5y09e/b09BkwYMB523v06FEZhwLgP7ZO/UrfNrpbbfrepE45i+SvYmU26qlt736t9s416vBSDxJSAICH6WdKFy1aJLvdrhkzZig2NlZTpkxRQkKCsrKy1KhRo/P6L1myREVFRZ71vLw8tW/fXg8++KBXvx49emju3LmedauV+9SAinau4H3A5BS1d30t6WzB+/XXPqSGb4xSzEPtTY4QAFBVmZ6UTp48WYMHD9bAgQMlSTNmzNCyZcs0Z84cjRo16rz+DRp4l4dZuHCh6tSpc15SarVaZbPZShVDYWGhCgsLPesul8vXwwBqtEsVvO9EwXsAwCWYevm+qKhIGzduVHx8vKfNz89P8fHxysjIKNUYs2fPVt++fVW3bl2v9vT0dDVq1Eht2rTRkCFDlJeXd8ExUlJSFBwc7FnCw8PLdkBADVPoKtSaxFnKufoGdXrnYbU5tVUnVFfp0c/q2Ma9un3nTDUjIQUAlIKpSemRI0fkdrsVGhrq1R4aGiqHw3HJ/TMzM7Vjxw4NGjTIq71Hjx6aP3++0tLSNGHCBK1evVp33XWX3G53ieMkJSXJ6XR6lpycnLIfFFADnHCcUPq9k3W0fkvd/n8fV/PTP+qopYHS7xir0z9mq+uGSbLxBiYAgA9Mv3x/OWbPnq2IiAjFxMR4tfft29fz54iICEVGRqpVq1ZKT09Xt27dzhvHarVyzylQChS8BwBUFFPPlIaEhMjf31+5uble7bm5uZe8H7SgoEALFy7UY489dsnPadmypUJCQvTjjz9eVrxATXVwwy9K7/CsAq5vpq6rx6qBcVT7rmqtr/q/rwbH9qjr0md0NQkpAOAymJqUBgQEKDo6WmlpaZ624uJipaWlKS4u7qL7Ll68WIWFherXr98lP+fnn39WXl6eGjdufNkxAzXJ/rQftebGx9WgY0t13ThZV6tAu2pH6ZsRixR+Ypc6z3tMVt7ABAAoB6Zfvrfb7erfv786dOigmJgYTZkyRQUFBZ6n8RMTE9WkSROlpKR47Td79mz17t1bDRs29Go/ceKExo4dq/vvv182m0179uzRyJEj1bp1ayUkJFTacQFXst8WvG/2m4L3Rc8mUV8UAFAhTE9K+/Tpo8OHDys5OVkOh0NRUVFavny55+Gn7Oxs+fl5n9DNysrS119/rRUrVpw3nr+/v7Zt26bU1FTl5+crLCxM3bt317hx47hvFLiEbe9+rcKXU9Tx8OeetsxGPRX4cpLaD7nVxMgAANWdxTAMw+wgqhqXy6Xg4GA5nU4FBQWZHQ5QoYxiQxteXa6AN18vseB9GwreVygz5puyfqaFE+TVRmX/8vPdqT58/e74Mt/4fKbU7XbL39/fs75+/XoVFhYqLi5OV111la/DATCJu8it9c//XQ1mjVfHX7dIOlfwvr+aTR1JwXsAQKUq9YNOBw8e1G233Sar1aouXbro2LFjuueeexQXF6euXbuqXbt2OnjwYEXGCqAcFLoK9VX/988WvH+7r274dcvvCt6/R8F7AEClK3VS+sILL8gwDH3yySdq3Lix7rnnHrlcLuXk5Gjfvn36wx/+oNdee60iYwVwGX5b8L7z/MH/LXjf9WUV/bCfgvcAAFOV+vL9//t//09LlizRLbfcoltvvVUhISFauXKlmjRpIkl65ZVXNHjw4AoLFEDZHNtzVFsHvaPI1W9T8B4AUGWVOik9duyYJwFt0KCB6tSpo2bNmnm2t27dmsv3QBVycMMvynpysjpsnKmuKpAk7buqtXIefkEx7zyqrtQXBQBUIaVOShs1aqSDBw8qPDxckvT000+rQYMGnu3Hjh1T3bp1yz9CAD7Zn/aj9j89UbG7UtVVRZKkrMD2yns8SbFvPKDmAf6XGAEAgMpX6qQ0KipKGRkZnvfMjx8/3mv7119/rcjIyPKNDkCpZX28VXnPpVDwHgBwRSp1Uvrpp59edHvHjh3VpUuXyw4IgG9KKnj/7R/ulvXlJLUfepuJkQEAUHrl9kanc2dQAVS8/xa8T1F711eSvAved6TgPQDgCmP6a0YBlB4F7wEA1RVJKXAFKHQVKnPY/1XTjyaq0+ndkqQTqqsN0U+ozQy7bu/QxOQIAQC4PCSlQBV2wnFCG554T23+NVmdi3+RJB21NNC2Ln9R5HtPq+t1DU2OEACA8kFSClRBJRe8D1PWPc+qw8zHKXgPAKh2SEqBKoSC9wCAmqrcktL+/fsrJydH//73v8trSKDGoOA9AKCmK7ektEmTJvLz8yuv4YAaIevjrcp7frxisz/+TcH721T07IsUvAcA1CjllpS+/vrr5TUUUO1R8B4AAG+XlZQahiFJslg4mwNcygUL3oc/eLbgfZ8ocwMEAMBEZbrePnv2bLVr106BgYEKDAxUu3bt9P7775d3bEC14C5y65tnPlbW1Ter45i71d71lYp0ldbcMFg//78sdcpeqDYkpACAGs7npDQ5OVnDhw9Xr169tHjxYi1evFi9evXSM888o+Tk5DIFMW3aNDVv3lyBgYGKjY1VZmbmBfvOmzdPFovFawkMDPTqYxiGkpOT1bhxY9WuXVvx8fHavXt3mWIDyqrQVaiv+r+v7KtvVKcpfXTDr1t0QnWVHm1X3rd7dfvO99SMNzABAHCW4aOQkBBjwYIF57UvWLDAaNiwoa/DGQsXLjQCAgKMOXPmGN99950xePBg45prrjFyc3NL7D937lwjKCjIOHjwoGdxOBxefcaPH28EBwcbS5cuNbZu3Wr86U9/Mlq0aGH8+uuvpYrJ6XQakgyn0+nz8QDHDx43Vt072Tjg18QwJMOQjKOW+saqri8beT8cMTs8VDFmzDdl/cz/fJ1ZqsFS2cw+Xhbzvju+zDc+nyk9ffq0OnTocF57dHS0zpw543NSPHnyZA0ePFgDBw5U27ZtNWPGDNWpU0dz5sy54D4Wi0U2m82zhIaGerYZhqEpU6bopZde0r333qvIyEjNnz9fBw4c0NKlS32ODyitY3uOKv3OV1QU1kxdP7WrcfEvOugXpvQ/vamrDmSr66oxasAbmAAAKJHPSemjjz6q6dOnn9f+3nvv6ZFHHvFprKKiIm3cuFHx8fH/DcjPT/Hx8crIyLjgfidOnFCzZs0UHh6ue++9V999951n2969e+VwOLzGDA4OVmxs7AXHLCwslMvl8lqA0nJsOqD0Ds+pVutmZxNP46j2XdVaax59Tw2O/aSun9p1NW9gAgDgosr09P3s2bO1YsUK3XLLLZKk9evXKzs7W4mJibLb7Z5+kydPvug4R44ckdvt9jrTKUmhoaHatWtXifu0adNGc+bMUWRkpJxOpyZNmqROnTrpu+++U9OmTeVwODxj/H7Mc9t+LyUlRWPHjr34QQO/s//fe7T/qYmK3TWPgvcAAFwmn5PSHTt26Oabb5Yk7dmzR5IUEhKikJAQ7dixw9OvospExcXFKS4uzrPeqVMn3XjjjZo5c6bGjRtXpjGTkpK8kmmXy6Xw8PDLjhXV0w9/36Yjz6acX/DenqQOo++i4D0AAGXgc1K6atWqcvvwkJAQ+fv7Kzc316s9NzdXNputVGNcddVVuummm/Tjjz9Kkme/3NxcNW7c2GvMqKioEsewWq2yWnmnOC5u2/S1OvVyimIOLdP1/2mj4D0AAOXD53tKDx8+fMFt27dv92msgIAARUdHKy0tzdNWXFystLQ0r7OhF+N2u7V9+3ZPAtqiRQvZbDavMV0ul9avX1/qMYFzzhW83xp8uyKH3qaYQ8vklp++Ce+jrIWb1fHQMkWSkAIAcNl8TkojIiK0bNmy89onTZqkmJgYnwOw2+2aNWuWUlNTtXPnTg0ZMkQFBQUaOHCgJCkxMVFJSUme/q+88opWrFihn376SZs2bVK/fv20f/9+DRo0SNLZ2wZGjBihV199VZ999pm2b9+uxMREhYWFqXfv3j7Hh5rpXMH7XVdHq8Pou/5b8L7NIOWs2EXBewAAypnPl+/tdrvuv/9+DRw4UJMnT9bRo0eVmJio7du3a8GCBT4H0KdPHx0+fFjJyclyOByKiorS8uXLPQ8qZWdny8/vv7nzsWPHNHjwYDkcDtWvX1/R0dH65ptv1LZtW0+fkSNHqqCgQI8//rjy8/N12223afny5ecV2Qd+r+hEkdY//X/VdMEEdTp99oULJ1RXG6KfUJsZdt3eoYnJEQIAUD1Zzha19c3mzZv16KOPqrCwUEePHlVsbKzmzJlT6vtAqzqXy6Xg4GA5nU4FBQWZHQ4qQcGhAn37+Htq88831bj4F0nSMUt9bb39L4qcNYz6oqgwZsw3Zf3MCnp+FSbw/Zf/8vDdqT58/e74Mt+UqSRU69at1a5dO/3jH/+QdPZsZ3VJSFGzHNtzVFsHT1VE+tvqauRJkg76hSnrnmfVYebj6kp9UQAAKoXP95SuXbtWkZGR2r17t7Zt26bp06dr2LBh6tOnj44dO1YRMQLl7vcF7xsaedpfqxUF7wEAMInPSemdd96pPn36aN26dbrxxhs1aNAgbd68WdnZ2YqIiKiIGIFys//fe7TmxidUP7qFum58U/V0QlmBkfpm2EdqcnyXbp8/WNYgyoMBAFDZfL58v2LFCnXp0sWrrVWrVlq7dq1ee+21cgsMKE8UvAcAoGrzOSn9fUJ6jp+fn0aPHn3ZAQHlqeSC93cpIDlJ7Z/ubGpsAADgv0p9+f7uu++W0+n0rI8fP175+fme9by8PK+yTIBZzhW833JNlwsUvP+chBQAgCqm1Enpl19+qcLCQs/666+/rqNHj3rWz5w5o6ysrPKNDvDB7wveRznXUPAeAIArRKkv3/++nGkZypsCFaKkgvcFqqNvb/5PwfuOTU2OEAAAXEqZ6pQCVUHBoQJteGKWrv9skjqXUPC+KwXvAQC4YpQ6KbVYLLL87pUMv18HKsNvC953+U/Be4dfY+26+1lFz3xcXcPqmRwhAADwlU+X7wcMGCCr9WwNx1OnTunJJ59U3bp1JcnrflOgIuRuOaidgycresMMddUJSdL+Wq20/+EXFDs1UV2pLwoAwBWr1Elp//79vdb79et3Xp/ExMTLjwj4nf3/3qP9T01U7K556qoiSVJWYKTyBicpZuIDahbIXSgAAFzpSv1rPnfu3IqMAzjPD3/fpsPPjdct+xd5Ct5vq3erCp99kYL3AABUM5xiQpWzfeY3+jX5dQreAwBQg5CUokowig1tfP1L1ZqUoijnGklSsSxaF/6gGkwYpY4P32RyhAAAoCKRlMJU7iK3Mkct0TUzUtTh182SpCJdpXVt+uvad0aq0/9cZ3KEAACgMpCUwhRFJ4q0ftgHavrhBMWd/kESBe8BAKjJSEpRqc4VvL/un2+qs/tnSRS8BwAAJKWoJPl7j2nLoKmKWPUWBe8BAMB5SEpRoS5Y8L7PSMW+25+C9wAAQJLkZ3YAkjRt2jQ1b95cgYGBio2NVWZm5gX7zpo1S507d1b9+vVVv359xcfHn9d/wIABnteinlt69OhR0YeB39j/7z1a0/ZJXXNTc3XdMEn1dEJZgZH6ZthHanJ8l27/4HFZSUgBAMB/mJ6ULlq0SHa7XWPGjNGmTZvUvn17JSQk6NChQyX2T09P18MPP6xVq1YpIyND4eHh6t69u3755Revfj169NDBgwc9y0cffVQZh1Pj/fD3bVrb/M9q2u163b5zpqwq0rZ6t+rbMf/S9QVb1OntvqrFG5gAAMDvWAzDMMwMIDY2Vh07dtTUqVMlScXFxQoPD9ewYcM0atSoS+7vdrtVv359TZ061fOa0wEDBig/P19Lly4tVQyFhYUqLCz0rLtcLoWHh8vpdCooKMj3g6qBzha8T1HMoX952r4N6aGAMS9S8B64CJfLpeDg4Eqdb8r6mRZeolZtVPYvP9+d6sPX744v842pZ0qLioq0ceNGxcfHe9r8/PwUHx+vjIyMUo1x8uRJnT59Wg0aNPBqT09PV6NGjdSmTRsNGTJEeXl5FxwjJSVFwcHBniU8PLxsB1TDGMWGNrz2pbZc00URT96qmEP/UrEs+ib8Ie1asEkdD39BQgoAAErF1KT0yJEjcrvdCg0N9WoPDQ2Vw+Eo1RgvvPCCwsLCvBLbHj16aP78+UpLS9OECRO0evVq3XXXXXK73SWOkZSUJKfT6VlycnLKflA1gLvIrQz7Yu26OlodXuqhKOcaFekqfXX9Y9q/fJc6ZS/SDbyBCQAA+OCKvrlv/PjxWrhwodLT0xUYGOhp79u3r+fPERERioyMVKtWrZSenq5u3bqdN47VapXVykM3l3KpgvedKXgPAADKyNQzpSEhIfL391dubq5Xe25urmw220X3nTRpksaPH68VK1YoMjLyon1btmypkJAQ/fjjj5cdc01UcKhAq/93io5c00qd5z2mFqd/0DFLfaXfnqxTu/ar68bJakxCCgAALoOpSWlAQICio6OVlpbmaSsuLlZaWpri4uIuuN/EiRM1btw4LV++XB06dLjk5/z888/Ky8tT48aNyyXumiJ/7zGldxunU7Zm6rL0GYW5f5bDr7HS75mkWj/vV9fVY9WwTYjZYQIAgGrA9Mv3drtd/fv3V4cOHRQTE6MpU6aooKBAAwcOlCQlJiaqSZMmSklJkSRNmDBBycnJWrBggZo3b+659/Tqq6/W1VdfrRMnTmjs2LG6//77ZbPZtGfPHo0cOVKtW7dWQkKCacd5JaHgPQAAqGymJ6V9+vTR4cOHlZycLIfDoaioKC1fvtzz8FN2drb8/P57Qnf69OkqKirSAw884DXOmDFj9PLLL8vf31/btm1Tamqq8vPzFRYWpu7du2vcuHHcN3oJ2ek/ad/QiYrdOVddVSRJygqMVN7gJMVMfEDNqC8KAAAqiOl1SqsiM+oGmumHf2zX4WfH65b9C+WvYknStnq3qtCepA7Jd8viR4E5oKJQpxRmoE4pyqoi65Ry6qsG+23B++v/00bBewAAYAaS0hrGKDa0MWWFar2RoijnaklSsSxa3/QB1Z+YpI7UFwUAACYgKa0h3EVuZSZ9omump6jDr5skSUW6SuuvT1TTt0cqLuH6S4wAAABQcUhKq7kLFbzfcNPjun7msxS8BwAAVQJJaTVVcKhAG558X9d9Nkmd3T9Lko5Z6mtr52GKeG+YulBfFAAAVCEkpdVM/t5j2jJoqiJWvaUuRp4kyeHXWLvuflbRMx9X17B6JkcIAABwPpLSaiJ3y0HtfPxviv52+m8K3rfU/j4vKGZqorpeE2hyhAAAABdGUnqFO1fwPmbnPHVVoaT/FLwfNEoxbzxIwXsAAHBFIGO5Qv224P21noL3nVRof5GC9wAA4IpDUnqF2f5ehn4d/fr5Be+TkxT5VGeSUQAAcEUiKb0CXKzg/TXjR6njIzebHCEAAMDlISmtwih4DwAAagqS0iqIgvcAAKCmISmtQkoqeJ9vuUZbOv+FgvcAAKBaIymtAvL3HtOWwdPU7t9vqYtxRNJ/Ct7fZVf0e09Q8B4AAFR7JKUmumDB+4dGKmZafwreAwCAGoOk1ATZ6T9p31NvKOb7uZ6C9z8ERujIoCQK3gMAgBqJ7KcSnSt4H7t/ka6VW9LZgvenRiSp48s9dT01RgEAQA1FUloO3EVubX/3K53cc1B1WjVWxNDO8g/w92zf/l6GTianKDb3n56C9xsaJuiqMS9S8B4AAECSn9kBSNK0adPUvHlzBQYGKjY2VpmZmRftv3jxYt1www0KDAxURESEPv/8c6/thmEoOTlZjRs3Vu3atRUfH6/du3dXSOzrRi5Rbp3minrmDnWa+mdFPXOHcus0V8bz/9DGlBXaXP8ORTzRSbG5/1SxLMpo+qB2frBRHY4sV/tht5OQAgAAqAokpYsWLZLdbteYMWO0adMmtW/fXgkJCTp06FCJ/b/55hs9/PDDeuyxx7R582b17t1bvXv31o4dOzx9Jk6cqLffflszZszQ+vXrVbduXSUkJOjUqVPlGvu6kUsU88YDsv2nfNM5NvfPumXSA4p+MUE35aerSFfpq+v+j/Z9vlNxOR/rRt7ABAAA4MViGIZhZgCxsbHq2LGjpk6dKkkqLi5WeHi4hg0bplGjRp3Xv0+fPiooKNC//vUvT9stt9yiqKgozZgxQ4ZhKCwsTM8++6yee+45SZLT6VRoaKjmzZunvn37XjIml8ul4OBgOZ1OBQUFldjHXeRWbp3msrl/vmBmXyyL1rQfputnPqew2PBLfi6Amqc0801V+UwLF3aqjcr+5ee7U334+t3xZb4x9UxpUVGRNm7cqPj4eE+bn5+f4uPjlZGRUeI+GRkZXv0lKSEhwdN/7969cjgcXn2Cg4MVGxt7wTELCwvlcrm8lkvZ/u5XCrtIQipJfjJ0zYD/JSEFAAC4BFOT0iNHjsjtdis0NNSrPTQ0VA6Ho8R9HA7HRfuf+6cvY6akpCg4ONizhIdfOok8uefgJfv40g8AAKAmM/2e0qogKSlJTqfTs+Tk5FxynzqtGpdq7NL2AwAAqMlMTUpDQkLk7++v3Nxcr/bc3FzZbLYS97HZbBftf+6fvoxptVoVFBTktVxKxNDOOuDfVMUq+UaZYln0i3+4IoZ2vuRYAAAANZ2pSWlAQICio6OVlpbmaSsuLlZaWpri4uJK3CcuLs6rvyStXLnS079Fixay2WxefVwul9avX3/BMcvCP8Bf2fa3zsb8u8T03HqOfYpXvVIAAACUzPTL93a7XbNmzVJqaqp27typIUOGqKCgQAMHDpQkJSYmKikpydN/+PDhWr58ud58803t2rVLL7/8sjZs2KCnn35akmSxWDRixAi9+uqr+uyzz7R9+3YlJiYqLCxMvXv3LtfYb5l4nzKf/7sc/k282g/6N1Xm83/XLRPvK9fPAwAAqK5Mf6NTnz59dPjwYSUnJ8vhcCgqKkrLly/3PKiUnZ0tP7//5s6dOnXSggUL9NJLL+nFF1/Uddddp6VLl6pdu3aePiNHjlRBQYEef/xx5efn67bbbtPy5csVGBhYqpjOVckqzVP4bV+Kl3vkNn39/jf6da9DtVvY9MdBndQ2wL9U+wOo2c7NE5VZnc+XOe63nM6KiAZmqOyfJ7471Yev3x1f5jjT65RWRT///HOpnsAHgPKSk5Ojpk2bVspnMccBqGylmeNISktQXFysAwcOqF69erKUsuKvy+VSeHi4cnJyKq0AdkWqTsdTnY5F4niqOl+PxzAMHT9+XGFhYV5XhSpSWea4mqC6fRdRufj+lMyXOc70y/dVkZ+fX5nPWJT26f0rRXU6nup0LBLHU9X5cjzBwcEVHI23y5njaoLq9l1E5eL7c77SznGmP+gEAAAAkJQCAADAdCSl5cRqtWrMmDGyWq1mh1IuqtPxVKdjkTieqq66HU9Nwr87XA6+P5ePB50AAABgOs6UAgAAwHQkpQAAADAdSSkAAABMR1IKAAAA05GUAgAAwHQkpeVg2rRpat68uQIDAxUbG6vMzEyzQyqTlJQUdezYUfXq1VOjRo3Uu3dvZWVlmR1WuRk/frwsFotGjBhhdihl9ssvv6hfv35q2LChateurYiICG3YsMHssHzmdrs1evRotWjRQrVr11arVq00btw4XSnFQNasWaNevXopLCxMFotFS5cu9dpuGIaSk5PVuHFj1a5dW/Hx8dq9e7c5waJcXCnfTeBKRlJ6mRYtWiS73a4xY8Zo06ZNat++vRISEnTo0CGzQ/PZ6tWr9dRTT2ndunVauXKlTp8+re7du6ugoMDs0C7bt99+q5kzZyoyMtLsUMrs2LFjuvXWW3XVVVfpiy++0Pfff68333xT9evXNzs0n02YMEHTp0/X1KlTtXPnTk2YMEETJ07UO++8Y3ZopVJQUKD27dtr2rRpJW6fOHGi3n77bc2YMUPr169X3bp1lZCQoFOnTlVypLhchw8fVnFxsSwWi9mhANWfgcsSExNjPPXUU551t9tthIWFGSkpKSZGVT4OHTpkSDJWr15tdiiX5fjx48Z1111nrFy50ujSpYsxfPhws0MqkxdeeMG47bbbzA6jXPTs2dP4P//n/3i13XfffcYjjzxiUkRlJ8n45JNPPOvFxcWGzWYz3njjDU9bfn6+YbVajY8++siECFFWmzdvNm688UZjzZo1ZocC1AicKb0MRUVF2rhxo+Lj4z1tfn5+io+PV0ZGhomRlQ+n0ylJatCggcmRXJ6nnnpKPXv29Pr3dCX67LPP1KFDBz344INq1KiRbrrpJs2aNcvssMqkU6dOSktL0w8//CBJ2rp1q77++mvdddddJkd2+fbu3SuHw+H1fQsODlZsbGy1mBdqiq1bt+qWW27Rn/70J3Xu3NnscHCFMv5z28f27du1aNEi/fOf/9TevXtNjqrqqmV2AFeyI0eOyO12KzQ01Ks9NDRUu3btMimq8lFcXKwRI0bo1ltvVbt27cwOp8wWLlyoTZs26dtvvzU7lMv2008/afr06bLb7XrxxRf17bff6i9/+YsCAgLUv39/s8PzyahRo+RyuXTDDTfI399fbrdbr732mh555BGzQ7tsDodDkkqcF85tQ9W2bds23XrrrXruuef06quvetpPnTqlwMBAEyPDlcTtdsvf319LlizRiBEjFBwcrLp166qwsFDvv/++oqOjzQ6xyuFMKUr01FNPaceOHVq4cKHZoZRZTk6Ohg8frg8//LBa/JAUFxfr5ptv1uuvv66bbrpJjz/+uAYPHqwZM2aYHZrPPv74Y3344YdasGCBNm3apNTUVE2aNEmpqalmh4Ya7uDBg4qKitJDDz3klZCOHTtWL7/8ss6cOWNidLgSuFwuSZK/v79WrVqlwYMH68UXX9T27ds1evRo7dixQ3fffbfWrl1rcqRVD0npZQgJCZG/v79yc3O92nNzc2Wz2UyK6vI9/fTT+te//qVVq1apadOmZodTZhs3btShQ4d08803q1atWqpVq5ZWr16tt99+W7Vq1ZLb7TY7RJ80btxYbdu29Wq78cYblZ2dbVJEZff8889r1KhR6tu3ryIiIvToo4/qmWeeUUpKitmhXbZz/+1Xt3mhpqhTp446deqkjIwM7dixQ9LZB9fefPNN3X777apViwuMuLBNmzapQYMG+v7773Xy5En94x//0NChQ/Xkk0/qwIEDGjp0qO6//37dcsst6t27tzZu3Gh2yFUKSellCAgIUHR0tNLS0jxtxcXFSktLU1xcnImRlY1hGHr66af1ySef6N///rdatGhhdkiXpVu3btq+fbu2bNniWTp06KBHHnlEW7Zskb+/v9kh+uTWW289r0TXDz/8oGbNmpkUUdmdPHlSfn7e04+/v7+Ki4tNiqj8tGjRQjabzWtecLlcWr9+/RU5L9Q0wcHB+uKLL9S4cWPdf//9GjFihN544w0tWbJEd999t9nhoQrbunWr7rjjDo0YMUJt27ZVnTp19Oijj6p79+7Kz8/Xn/70J/Xo0UMLFy7UwIEDlZeXp5iYGO41/y2zn7S60i1cuNCwWq3GvHnzjO+//954/PHHjWuuucZwOBxmh+azIUOGGMHBwUZ6erpx8OBBz3Ly5EmzQys3V/LT95mZmUatWrWM1157zdi9e7fx4YcfGnXq1DE++OADs0PzWf/+/Y0mTZoY//rXv4y9e/caS5YsMUJCQoyRI0eaHVqpHD9+3Ni8ebOxefNmQ5IxefJkY/Pmzcb+/fsNwzCM8ePHG9dcc43x6aefGtu2bTPuvfdeo0WLFsavv/5qcuQoyeHDh41169YZmzZt8rQdP37cuPvuuw2LxWLMmDHDxOhwJdi2bZtRu3ZtY/To0V7t534/V6xYYcTFxRl79+41DMMw1q1bZ/Ts2dP4y1/+Yuzatauyw62ySErLwTvvvGNce+21RkBAgBETE2OsW7fO7JDKRFKJy9y5c80OrdxcyUmpYRjGP//5T6Ndu3aG1Wo1brjhBuO9994zO6QycblcxvDhw41rr73WCAwMNFq2bGn89a9/NQoLC80OrVRWrVpV4n8r/fv3NwzjbFmo0aNHG6GhoYbVajW6detmZGVlmRs0SvT9998bXbp0Me655x4jMTHRa5vT6TT+53/+x2jZsqWxdetWkyJEVZednW384Q9/MO69916v9smTJxvPPPOM4Xa7jYULFxp+fn7Gd999ZxiGYbz44ovGww8/bBw/ftyEiKsui2HwmgoAQM2zfft2de3aVUOHDtXgwYN17bXXSpK2bNkim80mm82mEydOqFevXtq3b58+++wzRUREmBw1qppt27YpMTFRLVu21KBBg3T33XfrjTfe0Msvv6x//vOfuvPOO5Wdna2hQ4dq7dq1at++vTZs2KCMjAy+T79DUgoAqHEOHDig7t27q3v37po8ebKnfcKECUpKStKoUaP07LPPqmHDhjpx4oR69+6tb7/9VhkZGec9cIiayeVyKSAgQIGBgcrMzNTIkSPVsGFDXX311fr888+1aNEi3XnnnZ7+mzdv1r///W8dPXpUiYmJatOmjYnRV008RggAqHE2btyowMBAPfHEEyouLpafn58mTZqkcePGafjw4Zo4caIsFovsdrsaNmyoTz75RI888ogCAgLMDh1VwMGDB5WYmKh7771Xjz32mGJiYjRhwgS98MILWr58uUaNGuVJSM+cOaNatWrppptu0k033STDMHht7QWQlAIAapzVq1fr6NGjnrNVZ86ckc1m09KlSxUfH6+YmBg98sgjKioq0tixY1WvXj19+umnJBOQdPZNh/7+/lqwYIECAwP1yCOPKDY2Vn/7299kt9v17bffatmyZerZs6dq1arl+R8fXBx/QwCAGmH37t2eUl316tWTn5+fDh8+LLfbrVq1aumRRx5RfHy8DMPQww8/rMcee0zr1q3zlI8jIYUknT59WlarVZ9++qmaNm2q9957Tx9++KF+/fVX3XTTTZowYYKOHz+ud999V1988YUkeSWkfI8ujKQUAFDtbdmyRTfffLOn1m9cXJx++uknLV26tMSaxadPn5a/v79iY2M5wwVJ0vHjxyVJV111lSTJarVq/vz5uvbaazVz5kxPYnruUn5RUZFef/11rVixwsywryj8lwYAqNa2bt2qW2+9VU8//bSGDh0qSeratasGDBigJ554Qh999JGk/57BKi4u1pgxY/TZZ5/p8ccf9yQhqLl27typpk2bqm/fvnrxxRf1008/6cCBAwoMDNSHH36oG264Qe+++64++OADnTx5UjExMXr55Zd1zTXX6MYbbzQ7/CsGT98DAKqtbdu2KS4uTiNGjNBrr73maV+1apUcDocWLVqkf/7zn3ryySd166236ujRo1q3bp0+//xzpaWl6aabbjIxelQFZ86c0fz58zVo0CDZbDa1aNFCP/30k0JCQtSjRw/16NFDHTt21IABA/Trr7/qgQce0J///GfVrl1bp06dUmBgoNmHcMXgQScAQLWUk5Ojbt266Z577vFKSF955RXNnTtXaWlpio6O1s0336y33npLH374oRo3bqyoqCh9/fXXlH6CvvvuOy1ZskQjRozQoUOH9Ne//lXvvPOOQkJC9P3332v+/PlauHChbDabQkNDtXr1amVlZXkefrJarWYfwhWFy/cAgGrJ7XarRYsWOnXqlNauXStJGj9+vN555x1NmzZNLVu21PXXX6/k5GR999132rJli9auXat58+aRkEJbt25VRESEatWqpXr16mnUqFGy2+169NFHdeDAAQ0ZMkSrVq3S1q1b9dBDD+mPf/yjAgICdPLkSd1yyy2SeKjJV1y+BwBUW7t379Zf/vIXBQQEKDQ0VEuXLtUHH3yg7t27e9WL/P7770lE4fH999+rQ4cOGjlypF5++WWvbS+88IL+9re/ac6cOerXr5/Xtp9++kn16tXTH/7wh0qMtvogKQUAVGs//PCDnn76aX399dcaN26cnn32WZ376bNYLBo9erTmzZun7du3Kzg4mLNbNdyOHTt0xx136A9/+IO+//57SWerMfz2gbdRo0Zp8uTJSk1N1cMPP2xWqNUOl+8BANXa9ddfr+nTp6tz585KS0vTV199JYvFIovFouTkZE2aNElLly7VNddcQ0Jaw23dulWxsbFq166dnE6nhg8fLulsGSi32+3pN378eNntdg0ePFhz5841K9xqhzOlAIAa4dylfMMwlJKSopUrV2rMmDH6+uuvFR0dbXZ4MNmGDRvUqVMn/fWvf9VLL72k2bNn669//av+/Oc/66233pJ09j7l39a1ffrpp7V48WLt3r1bQUFBZoVebZCUAgBqjN27d8tutyszM1PHjh1TRkYGCSkkSWvWrNE//vEPTwLqdDq1aNGiSyamhw4dUqNGjUyJubohKQUA1ChZWVkaOXKkXn/9df3xj380OxxUQecegnO5XFq4cOF5iemZM2dUqxZVNcsbSSkAoMb5/YMrwIX8NjF99NFHNXnyZLNDqrZI8wEANQ4JKUorKChIffv2lZ+fnx5//HFZrValpKSYHVa1RFIKAABwEUFBQXrwwQd11VVXKS4uzuxwqi0u3wMAAJTCb1+4gPJHnVIAAIBSICGtWCSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdCSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdCSlAAAAMB1JKQAAAExHUgoAAADT/X/X2et+aLQhZAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_fitness(G1, exp_list_G1_and, P, measured_only=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G1_prep.plot(custom_edge_attr=cno.cno_style(P, flow_name=\"edge_activates\", scale=None, iexp=3))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Tasks: \n", - "1. modify the experiments so that it corresponds to a scenario where only EGF could activate RAS, but not TNFa. \n", - "2. modify the experiments so that it corresponds to a scenario where both TNFa or EGF could activate RAS.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inhibition: !EGF activates RAS\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's define the same network but now we will add an inhibition of EGF on RAS. This means that if EGF is active, RAS should be inactive.\n", - "Note the difference in the definition of the prior knowledge network.\n", - "\n", - "We also change the experiments so that the output RAS is active only when TNFa is active." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# Test graph\n", - "G2 = cn.Graph.from_sif_tuples(\n", - " [\n", - " (\"EGF\", -1, \"AND1\"),\n", - " (\"TNFa\", 1, \"AND1\"),\n", - " (\"AND1\", 1, \"Ras\"),\n", - " (\"EGF\", -1, \"Ras\"),\n", - " (\"TNFa\", 1, \"Ras\"),\n", - " ]\n", - ")\n", - "\n", - "# RAS is only active iff both EGF and TNFa are active -> we need to idetify the AND gate\n", - "exp_list_G2_egf = {\n", - " \"exp0\": {\"input\": {\"EGF\": 0, \"TNFa\": 0}, \"output\": {\"Ras\": 1}},\n", - " \"exp1\": {\"input\": {\"EGF\": 1, \"TNFa\": 0}, \"output\": {\"Ras\": 0}},\n", - " \"exp2\": {\"input\": {\"EGF\": 0, \"TNFa\": 1}, \"output\": {\"Ras\": 1}},\n", - " \"exp3\": {\"input\": {\"EGF\": 1, \"TNFa\": 1}, \"output\": {\"Ras\": 0}},\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G2_prep = cno.expand_graph_for_flows(G2, exp_list_G2_egf)\n", - "G2_prep.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "P2 = cno.cellnoptILP(G2_prep, exp_list_G2_egf, verbose=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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ERER2b5qN6UffPABVq9ejZdffhmDBg2CWq3GSy+9ZDaHnwQgIiIiopqo9o1O33zzDb766is89dRTAICxY8di8ODBiI6ONr1rKuGHRoiIiIioBqr9TunZs2fh7+9vety+fXukpKRgz549GD16dJVf4UlEREREdD/VbkoVCgVOnDhhNtayZUts27YNe/fuxdixY2u7NiIiIiKyE9VuSgcOHIg1a9ZUGPf29sbvv/+OvLy8Wi2MiIiIiOxHtT9TOnPmTBw5cqTS51q2bInt27dj69attVYYEREREdmPajelbdq0QZs2bap83tvbG2PGjKmVooiIiIjIvjA8n4iIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiERXa03pmDFjMHDgwNo6HBERERHZkWpHQt1Py5YtIZXyjVciIiIislytNaUffvhhbR2KiIiIiOzMA721KQgCBEGorVqIiIiIyE7VqCldvnw5/P394eTkBCcnJ/j7++Orr76q7dqIiIiIyE5Y3JTGx8dj0qRJGDp0KNavX4/169dj6NChmDJlCuLj42tURGJiInx9feHk5ASlUomMjIwq565atQoSicRsc3JyMpsjCALi4+Ph5eUFZ2dnqFQqHDt2rEa1EREREVHds/gzpUuWLMGyZcswcuRI09iwYcPQpUsXTJw4Ee+++65Fx1u3bh3UajWWLl0KpVKJTz/9FGFhYTh69ChatGhR6T5ubm44evSo6bFEIjF7ft68efj888+RlJQEPz8/zJw5E2FhYTh06FCFBpaIiIiIxGfxO6U3btxAjx49KowHBQXh5s2bFhewYMECjB8/HtHR0ejcuTOWLl0KFxcXrFixosp9JBIJFAqFafP09DQ9JwgCPv30U7z99tsYPnw4unTpgtWrV+PcuXPYtGmTxfURERERUd2zuCkdPXo0lixZUmH8yy+/xAsvvGDRscrLy5GZmQmVSnWnIKkUKpUKqampVe5XUlKCNm3awMfHB8OHD8fBgwdNz+Xl5UGr1Zod093dHUqlsspjlpWVQa/Xm21EREREZD01ioRavnw5tmzZgl69egEA0tPTkZ+fj6ioKKjVatO8BQsW3PM4Fy9ehMFgMHunEwA8PT1x5MiRSvfp2LEjVqxYgS5dukCn02H+/Pno3bs3Dh48iFatWkGr1ZqOcfcxbz93t4SEBMyePfveJ01EREREdcbipjQ3Nxfdu3cHAJw4cQIA4OHhAQ8PD+Tm5prm3f05z9oSEhKCkJAQ0+PevXujU6dO+OKLL/Dee+/V6JhxcXFmzbRer4ePj88D10pERERE1WNxU7pt27Zae3EPDw/IZDIUFRWZjRcVFUGhUFTrGI0aNUK3bt1w/PhxADDtV1RUBC8vL7NjBgYGVnoMuVwOuVxegzMgIiIiotpg8WdKL1y4UOVzOTk5Fh3L0dERQUFB0Gg0pjGj0QiNRmP2bui9GAwG5OTkmBpQPz8/KBQKs2Pq9Xqkp6dX+5hEREREZF0WN6UBAQHYvHlzhfH58+cjODjY4gLUajWWLVuGpKQkHD58GLGxsSgtLUV0dDQAICoqCnFxcab57777LrZs2YKTJ08iKysLo0aNwunTpxETEwPg1scGJk+ejPfffx8//vgjcnJyEBUVBW9vb0RERFhcHxERERHVPYsv36vVajzzzDOIjo7GggULcPnyZURFRSEnJwdr1qyxuIDIyEhcuHAB8fHx0Gq1CAwMRHJysulGpfz8fEild3rnK1euYPz48dBqtWjatCmCgoKwZ88edO7c2TRn2rRpKC0txYsvvoji4mL07dsXycnJzCglIiIiqqckQg2+vH7fvn0YPXo0ysrKcPnyZSiVSqxYsaLanwOt7/R6Pdzd3aHT6eDm5iZ2OURkw8RYb7jGkbXV0b3PJAJLu0ZL1huLL98DQPv27eHv749Tp05Br9cjMjLSZhpSIiIiIrI+i5vS3bt3o0uXLjh27BgOHDiAJUuWYOLEiYiMjMSVK1fqokYiIiIisnEWN6UDBw5EZGQk0tLS0KlTJ8TExGDfvn3Iz89HQEBAXdRIRERERDbO4hudtmzZgv79+5uNtWvXDrt378YHH3xQa4URERERkf2w+J3SuxtS04GkUsycOfOBCyIiIiIi+1PtpvTJJ5+ETqczPZ4zZw6Ki4tNjy9dumQWy0REREREVF3Vbkp/++03lJWVmR5/+OGHuHz5sunxzZs3cfTo0dqtjoiIiIjsQrWb0rvjTGsQb0pEREREVKka5ZQSEREREdWmajelEokEkru+kuHux0RERERENVHtSChBEDB27FjI5XIAwPXr1/Hyyy/D1dUVAMw+b0pEREREZIlqN6Vjxowxezxq1KgKc6Kioh68IiIiIiKyO9VuSleuXFmXdRARERGRHeONTkREREQkOjalRERERCQ6NqVEREREJDo2pUREREQkOjalRERERCQ6NqVEREREJDo2pUREREQkunrRlCYmJsLX1xdOTk5QKpXIyMiocu6yZcvQr18/NG3aFE2bNoVKpaowf+zYsaavRb29hYeH1/VpEBEREVENid6Urlu3Dmq1GrNmzUJWVha6du2KsLAwnD9/vtL5KSkpGDlyJLZt24bU1FT4+Phg0KBBOHv2rNm88PBwFBYWmrZvv/3WGqdDRERERDUgEQRBELMApVKJnj17YtGiRQAAo9EIHx8fTJw4ETNmzLjv/gaDAU2bNsWiRYtMX3M6duxYFBcXY9OmTdWqoaysDGVlZabHer0ePj4+0Ol0cHNzs/ykiIiqSa/Xw93d3arrjRivSfZNIhG7AqotlnaNlqw3or5TWl5ejszMTKhUKtOYVCqFSqVCampqtY5x7do13LhxA82aNTMbT0lJQYsWLdCxY0fExsbi0qVLVR4jISEB7u7ups3Hx6dmJ0RERERENSJqU3rx4kUYDAZ4enqajXt6ekKr1VbrGNOnT4e3t7dZYxseHo7Vq1dDo9Fg7ty52L59OwYPHgyDwVDpMeLi4qDT6UxbQUFBzU+KiIiIiCzmIHYBD2LOnDlYu3YtUlJS4OTkZBofMWKE6Z8DAgLQpUsXtGvXDikpKXjiiScqHEcul0Mul1ulZiIiIiKqSNR3Sj08PCCTyVBUVGQ2XlRUBIVCcc9958+fjzlz5mDLli3o0qXLPee2bdsWHh4eOH78+APXTERERES1T9Sm1NHREUFBQdBoNKYxo9EIjUaDkJCQKvebN28e3nvvPSQnJ6NHjx73fZ0zZ87g0qVL8PLyqpW6iYiIiKh2iR4JpVarsWzZMiQlJeHw4cOIjY1FaWkpoqOjAQBRUVGIi4szzZ87dy5mzpyJFStWwNfXF1qtFlqtFiUlJQCAkpISTJ06FWlpaTh16hQ0Gg2GDx+O9u3bIywsTJRzJCIiIqJ7E/0zpZGRkbhw4QLi4+Oh1WoRGBiI5ORk081P+fn5kErv9M5LlixBeXk5nn32WbPjzJo1C++88w5kMhkOHDiApKQkFBcXw9vbG4MGDcJ7773Hz40SERER1VOi55TWR8zwIyJrYU4p2QPmlNoOm80pJSIiIiIC2JQSERERUT3AppSIiIiIRMemlIiIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiETHppSIiIiIRMemlIiIiIhEx6aUiIiIiERXL5rSxMRE+Pr6wsnJCUqlEhkZGfecv379ejz66KNwcnJCQEAAfvnlF7PnBUFAfHw8vLy84OzsDJVKhWPHjtXlKRARERHRAxC9KV23bh3UajVmzZqFrKwsdO3aFWFhYTh//nyl8/fs2YORI0di3Lhx2LdvHyIiIhAREYHc3FzTnHnz5uHzzz/H0qVLkZ6eDldXV4SFheH69evWOi0iIiIisoBEEARBzAKUSiV69uyJRYsWAQCMRiN8fHwwceJEzJgxo8L8yMhIlJaW4ueffzaN9erVC4GBgVi6dCkEQYC3tzdef/11vPHGGwAAnU4HT09PrFq1CiNGjKhwzLKyMpSVlZke6/V6+Pj4QKfTwc3NrbZPmYjIRK/Xw93d3arrjRivSfZNIhG7AqotlnaNlqw3or5TWl5ejszMTKhUKtOYVCqFSqVCampqpfukpqaazQeAsLAw0/y8vDxotVqzOe7u7lAqlVUeMyEhAe7u7qbNx8fnQU+NiIiIiCwgalN68eJFGAwGeHp6mo17enpCq9VWuo9Wq73n/Nt/WnLMuLg46HQ601ZQUFCj8yEiIiKimnEQu4D6QC6XQy6Xi10GERERkd0S9Z1SDw8PyGQyFBUVmY0XFRVBoVBUuo9Cobjn/Nt/WnJMIiIiIhKXqE2po6MjgoKCoNFoTGNGoxEajQYhISGV7hMSEmI2HwC2bt1qmu/n5weFQmE2R6/XIz09vcpjEhEREZG4RL98r1arMWbMGPTo0QPBwcH49NNPUVpaiujoaABAVFQUWrZsiYSEBADApEmT0L9/f3z88ccYMmQI1q5diz/++ANffvklAEAikWDy5Ml4//338cgjj8DPzw8zZ86Et7c3IiIiqlXT7UACvV5f+ydMRPQPt9cZawahcI0ja9PpxK6Aaouly4ZFa5xQDyxcuFBo3bq14OjoKAQHBwtpaWmm5/r37y+MGTPGbP53330ndOjQQXB0dBQee+wxYfPmzWbPG41GYebMmYKnp6cgl8uFJ554Qjh69Gi16ykoKBAAcOPGjZvVtoKCggdaRy3BNY4bN27W3qqzxomeU1ofGY1GnDt3Dk2aNIGE4WpEVIcEQcDVq1fh7e0NqdQ6n6jiGkdE1mLJGsemlIiIiIhEJ/rXjBIRERERsSklIiIiItGxKSUiIiIi0bEpJSIiIiLRsSklIiIiItGxKSUiIiIi0bEpJSIiIiLRsSklIiIiItGxKSUiIiIi0bEpJSIiIiLROYhdQH3E74UmImux5HuhawvXOCKyFkvWODallTh37hx8fHzELoOI7EhBQQFatWplldfiGkdE1ladNY5NaSWaNGkC4Na/QDc3N5GrISJbptfr4ePjY1p3rIFrHBFZiyVrHJvSSty+nOXm5sYFm4iswpqX0bnGEZG1VWeN441ORERERCQ6NqVEREREJDo2pUREREQkOjalRERERCQ6NqVEREREJDrefV8LDOUG5CzeiWsnCuHSzgsBE/pB5igTuywiIiKiBkPUd0p37NiBoUOHwtvbGxKJBJs2bbrvPikpKejevTvkcjnat2+PVatWVZiTmJgIX19fODk5QalUIiMjo/aL/1vatA0ocvFF4JQB6L3oPwicMgBFLr5Im7ahzl6TiIiIyNaI2pSWlpaia9euSExMrNb8vLw8DBkyBAMGDEB2djYmT56MmJgY/Pbbb6Y569atg1qtxqxZs5CVlYWuXbsiLCwM58+fr/X606ZtQPBHz0JhOGM2rjCcRfBHz7IxJSIiIqomiSAIgthFALdCVTdu3IiIiIgq50yfPh2bN29Gbm6uaWzEiBEoLi5GcnIyAECpVKJnz55YtGgRgFvf8ezj44OJEydixowZ1apFr9fD3d0dOp2uymBpQ7kBRS6+UBjOVNrZGyFBoawVFNfyeCmfiKpUnfXGFl6TiOyTJetNg7rRKTU1FSqVymwsLCwMqampAIDy8nJkZmaazZFKpVCpVKY5lSkrK4Nerzfb7idn8U54V9GQAoAUAloaCpCzeOf9T4yIiIjIzjWoG520Wi08PT3Nxjw9PaHX6/HXX3/hypUrMBgMlc45cuRIlcdNSEjA7NmzLarl2onCas27uv8kgFCLjk1EVB9Z8ZtQqY5Z+xopf3ZsR13+7DSod0rrSlxcHHQ6nWkrKCi47z4u7byqdezAVa8hpe/buHj4woOWSURERGSzGlRTqlAoUFRUZDZWVFQENzc3ODs7w8PDAzKZrNI5CoWiyuPK5XK4ubmZbfcTMKEfzslawYjK//pnBHADDmiCUoTu/gAundtge+AknE3Nv/+JEhEREdmZBtWUhoSEQKPRmI1t3boVISEhAABHR0cEBQWZzTEajdBoNKY5tUXmKEO++rNbr3FXY3rrsQR71d8ibdoGHHLpARf8hf77P0eL3u2w85FonPyl6o8TEBEREdkbUZvSkpISZGdnIzs7G8CtyKfs7Gzk5996NzEuLg5RUVGm+S+//DJOnjyJadOm4ciRI1i8eDG+++47TJkyxTRHrVZj2bJlSEpKwuHDhxEbG4vS0lJER0fXev295j2NjKnfQytraTZeKGuFjKnfo/fHz6LX3H+j09UMZM3diqymA9EIN9Hv+Cr4DumM1FbP4vDXmbVeFxEREVGDI4ho27ZtAoAK25gxYwRBEIQxY8YI/fv3r7BPYGCg4OjoKLRt21ZYuXJlheMuXLhQaN26teDo6CgEBwcLaWlpFtWl0+kEAIJOp6vW/JtlN4V9n2wTdr+6Rtj3yTbhZtnNKufmfJUmpCmGC8KtzwoLAiD80exfwr5PtglGg9GiOomo4bN0vRHzNf+xbHFr4Ju1iX2+3MT72bFkvak3OaX1iTUy/I7/cBBa9Vz0OrkGDjAAAHIa98K11+LQc/ZTkDo0qE9WEFENNaScUt5BbTus/ZufPzu2w9KfHZvNKbUl7Yc/hr4nVqNw+zFs95+A65AjoCQNyg+H40Tjrtg94RvcvH5T7DKJiIiIrIJNqch8HvdD/5xE6PefQopyOvRogkfKctFnySica9IBO0YuwfXi62KXSURERFSn2JTWEy26KBCaNgfCqXyk/OsDXJA8jNY38/D42gnQNfdDypPzoD9z/2+aIiIiImqI2JTWM+5tHkLoljfhev4Utj/zOc7KfOBp1CL01+kwtm7DIH4iIiKySWxK6ykXDxf0/34iHi4+jl3jVuKE46N4SCg2C+I/l37/b54iIiIiagjYlNZzjo0d0fersfArPYi0qf8zC+J/uFdbBvETERGRTWBT2kBIHaToNe9pdLqagcyELdj30AAG8RMREZHNYFPawEikEgTN+Be6XfkduV+lIV0xHFIICDn7P3Qa3QN/eIQh+9MUCEbGzxIREVHDwaa0AfMfp4SycBOObcjBLr9RuAkZelzagsApA5Dr3hvpb/0I402j2GUSERER3RebUhvwyL/90ffkfxnET0RERA0Wm1IbwiB+IiIiaqjYlNogsyB+1fu4KPFgED8RERHVa2xKbZh7m4cQuvUtuJw/XXkQf7+ZDOInIiKieoFNqR2oMoh/1/sM4iciIqJ6gU2pHakYxB9kHsTf4f9w8tejYpdJREREdohNqR26E8S/1zyI/9hK+D7ZCamtnsPhb7LELpOIiIjsCJtSO2YWxL8sFemKYX8H8X+PTqOCGMRPREREVsOmlAAA/jG9oCz8ocog/oyZPzGIn4iIiOoMm1Iy888g/h2PxZqC+IPfH3YriP+VNQziJyIiolrHppQq5fO4Hx7PXVwxiH/xCzjbpCN2/Gcpg/iJiIio1tSLpjQxMRG+vr5wcnKCUqlERkZGlXNDQ0MhkUgqbEOGDDHNGTt2bIXnw8PDrXEqNqeyIP42N0/i8W9jbwXxD/kIV89dFbtMIiIiauBEb0rXrVsHtVqNWbNmISsrC127dkVYWBjOnz9f6fwNGzagsLDQtOXm5kImk+G5554zmxceHm4279tvv7XG6disKoP4f5kGQ6vWDOInIiKiByJ6U7pgwQKMHz8e0dHR6Ny5M5YuXQoXFxesWLGi0vnNmjWDQqEwbVu3boWLi0uFplQul5vNa9q0aZU1lJWVQa/Xm21UubuD+E86djQP4u82mUH8REREZDFRm9Ly8nJkZmZCpVKZxqRSKVQqFVJTU6t1jOXLl2PEiBFwdXU1G09JSUGLFi3QsWNHxMbG4tKlS1UeIyEhAe7u7qbNx8enZidkR24H8be5ehCpb3x/J4g/+zN49GrHIH4iIiKyiKhN6cWLF2EwGODp6Wk27unpCa1We9/9MzIykJubi5iYGLPx8PBwrF69GhqNBnPnzsX27dsxePBgGAyGSo8TFxcHnU5n2goK+E5fdckcZQj56BmzIH5H3GAQPxEREVlE9Mv3D2L58uUICAhAcHCw2fiIESMwbNgwBAQEICIiAj///DP27t2LlJSUSo8jl8vh5uZmtpFlqhXE/9l2BvETERFRpURtSj08PCCTyVBUVGQ2XlRUBIVCcc99S0tLsXbtWowbN+6+r9O2bVt4eHjg+PHjD1QvVc8/g/h3+75wJ4h/cihy3fsgY+ZPbE6JiIjIjKhNqaOjI4KCgqDRaExjRqMRGo0GISEh99x3/fr1KCsrw6hRo+77OmfOnMGlS5fg5eX1wDVT9T3yb3/0yfv6riD+VAS/PwzHXbowiJ+IiIhMRL98r1arsWzZMiQlJeHw4cOIjY1FaWkpoqOjAQBRUVGIi4ursN/y5csRERGB5s2bm42XlJRg6tSpSEtLw6lTp6DRaDB8+HC0b98eYWFhVjknMmcWxB88jUH8REREVIHoTWlkZCTmz5+P+Ph4BAYGIjs7G8nJyaabn/Lz81FYWGi2z9GjR7Fr165KL93LZDIcOHAAw4YNQ4cOHTBu3DgEBQVh586dkMvlVjknqlyLLgqEps+F8eRpBvETERGRGYkgCPxw3130ej3c3d2h0+l401MdunbxGva+9BXa/zAfLQ23Eg+KJQ8hu8+rCPhqEpp39BC5QqK6J8Z6U9PXlEjqsCiyKmv/5ufPju2w9GfHkvXG4ndK745VSk9Px44dO3Djxg1LD0V2zsXDBf3/9xoeLj6OndErzIL4nR5lED8REZE9qXZTWlhYiL59+0Iul6N///64cuUKnnrqKYSEhCA0NBT+/v4VLrMTVYdjY0f0WxFtFsTvimsM4iciIrIj1W5Kp0+fDkEQsHHjRnh5eeGpp56CXq9HQUEBTp06hYcffhgffPBBXdZKNs4siP/D37DvoVAG8RMREdmJan+m1NvbGxs2bECvXr1w+fJleHh4YOvWrXjiiScAAL///jvGjx+PEydO1GnB1sDPlNYfuV+loXRmApTaH01jfzQPg8PMOHSd+DgkUn5QiRo2fqaUxMDPlFJN1YvPlF65cgUtW7YEADRr1gwuLi5o06aN6fn27dvz8j3VusqD+H9jED8REZGNqXZT2qJFC7Om89VXX0WzZs1Mj69cuQJXV9farY7ob7eD+M9t+xM7Or9sFsR/zLUrg/iJiIgauGo3pYGBgUhNTTU9njNnjllTumvXLnTp0qV2qyO6S+vQtnj84BLo9uWZgvg7XM+5E8T/whcM4iciImqAai2nNCMjAy4uLvD396+Nw4mKnyltOIrzriB7fCL8f/8MHsJFAECRVIHD4WoELXsZTbybiFwh0b3xM6UkBn6mlGqqXnym9H6Cg4NtoiGlhuUhv6YI/X9vw1l7Ctuf/gznZK3gadQi9JdpMLRqjZTH43Hp6EWxyyQiIqL7EP1rRolqg2sLV/T/32vwKD5hHsS/8z0G8RMRETUAbErJptwdxH/YuftdQfzjkPfbn2KXSURERHdhU0o26XYQ/6Mlf9wVxL8CbcIfZRA/ERFRPcOmlGyaRCpBUNwgdLuyDTlf7EG651BIISDk7PfoNCoIf3iEI/uz7cw6JSIiEhmbUrIbAS+GQKn9EX9+fwC7fV+AAVLzIP74n9mcEhERiaTWmtIxY8Zg4MCBtXU4ojrT4ZkA9Mn7Gme3HTMP4n9vKI65dsWeid8yiJ+IiMjKaq0pbdmypdnXjhLVd2ZB/D2n4ioao8P1HPRe9B8G8RMREVlZrYXn2xKG59unKoP4B7+OoC9fYhA/1QmG55MYGJ5PNVVvw/MFQQB7WrIVZkH8//70ThD/5qm42aoNg/iJiIjqUI2a0uXLl8Pf3x9OTk5wcnKCv78/vvrqq9qujUgUri1c0X/DJLMg/qbClTtB/N2noHDvGbHLJCIisikWN6Xx8fGYNGkShg4divXr12P9+vUYOnQopkyZgvj4+BoVkZiYCF9fXzg5OUGpVCIjI6PKuatWrYJEIjHbnJyczOYIgoD4+Hh4eXnB2dkZKpUKx44dq1FtZL/MgvjV6+8E8e/7FM2D2zKIn4iIqDYJFvLw8BDWrFlTYXzNmjVC8+bNLT2csHbtWsHR0VFYsWKFcPDgQWH8+PHCQw89JBQVFVU6f+XKlYKbm5tQWFho2rRardmcOXPmCO7u7sKmTZuE/fv3C8OGDRP8/PyEv/76q1o16XQ6AYCg0+ksPh+yXUaDUfjjw9+Efe79BeHWx2oEAyTCnlbPCYfXZIldHjVQYqw3NX3Nv3/sudnAZm1iny838X52LFlvLH6n9MaNG+jRo0eF8aCgINy8aXmMzoIFCzB+/HhER0ejc+fOWLp0KVxcXLBixYoq95FIJFAoFKbN09PT9JwgCPj000/x9ttvY/jw4ejSpQtWr16Nc+fOYdOmTRbXR3Tb7SD+wOIU8yD+M+vx6H+64w+PcOxfuINZp0RERDVgcVM6evRoLFmypML4l19+iRdeeMGiY5WXlyMzMxMqlepOQVIpVCoVUlNTq9yvpKQEbdq0gY+PD4YPH46DBw+ansvLy4NWqzU7pru7O5RKZZXHLCsrg16vN9uI7sUsiL/Nf0xB/F1f64+ch/oyiJ+IiMhCD3SjU0xMDGJiYhAQEIBly5ZBKpVCrVabtvu5ePEiDAaD2TudAODp6QmtVlvpPh07dsSKFSvwww8/4Ouvv4bRaETv3r1x5sytG09u72fJMRMSEuDu7m7afHx87ls7EfB3EP+pb8yC+Ltc3cMgfiIiIgtZ3JTm5uaie/fuePjhh3HixAmcOHECHh4e6N69O3Jzc7Fv3z7s27cP2dnZdVAuEBISgqioKAQGBqJ///7YsGEDHn74YXzxxRc1PmZcXBx0Op1pKygoqMWKyR4wiJ+IiOjBOFi6w7Zt22rtxT08PCCTyVBUVGQ2XlRUBIVCUa1jNGrUCN26dcPx48cBwLRfUVERvLy8zI4ZGBhY6THkcjnkcnkNzoDInGegFzwz5qE4Lw4pMYsQsO0ztLl5Em3WvAzt2tk4MljNIH4iIqJKWPxO6YULF6p8Licnx6JjOTo6IigoCBqNxjRmNBqh0WgQEhJSrWMYDAbk5OSYGlA/Pz8oFAqzY+r1eqSnp1f7mEQP6iG/pgjVzIST9rQpiF9hLGQQPxERURUsbkoDAgKwefPmCuPz589HcHCwxQWo1WosW7YMSUlJOHz4MGJjY1FaWoro6GgAQFRUFOLi4kzz3333XWzZsgUnT55EVlYWRo0ahdOnTyMmJgbArTvzJ0+ejPfffx8//vgjcnJyEBUVBW9vb0RERFhcH9GDuDuIP69RBwbxExERVcLiy/dqtRrPPPMMoqOjsWDBAly+fBlRUVHIycnBmjVrLC4gMjISFy5cQHx8PLRaLQIDA5GcnGy6USk/Px9S6Z3e+cqVKxg/fjy0Wi2aNm2KoKAg7NmzB507dzbNmTZtGkpLS/Hiiy+iuLgYffv2RXJycoWQfSJruR3Eb1gahdS4jXhoyYfo9Nc+9N/3KcqDE7GzQxRafT4NfmEdxC6ViIhIFJJbobaW2bdvH0aPHo2ysjJcvnwZSqUSK1asqPbnQOs7vV4Pd3d36HQ6uLm5iV0O2SDBKCAzYQscPkpAoG47AMAICdJbPYum8+Lw6MhuIldI1iLGelPT15RI6rAosirLf/M/GP7s2A5Lf3YsWW9qFAnVvn17+Pv749SpU9Dr9YiMjLSZhpTIGiRSCXq8FXYriH/pbmS0eMosiH/vw4MZxE9ERHbF4qZ09+7d6NKlC44dO4YDBw5gyZIlmDhxIiIjI3HlypW6qJHIpgW81BvBRT+ZBfH3vJjMIH4iIrIrFjelAwcORGRkJNLS0tCpUyfExMRg3759yM/PR0BAQF3USGQXbgfxn9H8iR2dXkIZHE1B/H+6BjKIn4iIbJrFTemWLVswZ84cNGrUyDTWrl077N69Gy+99FKtFkdkj9oMbIfHDy1F8b5TpiD+jtcP3AniH/UlyvRlYpdJRERUq2p0o5Ot441OVJ8U511B9t9B/M2FSwAArdSLQfw2gjc6kRh4oxPVVL240enJJ5+ETqczPZ4zZw6Ki4tNjy9dumQWy0REteO+Qfz9Z+HysUtil0lERPRAqt2U/vbbbygru3PJ8MMPP8Tly5dNj2/evImjR4/WbnVEZGIWxD92+Z0g/h3vQt6hNYP4iYioQat2U3r3VX5e9ScSh2NjR/Rb+X9oXXIIqVO+w2HnbnDFNfTf9ymaB7fFzo4xyPvtT7HLJCIiskiNckqJSHwyRxlCFjyHR0sy8cf7ych27w9H3EC/P5ejTfijSPV5Hke+3Sd2mURERNVS7aZUIpFActcnle9+TETWV60g/kU7xS6TiIjonhyqO1EQBIwdOxZyuRwAcP36dbz88stwdXUFALPPmxKROAJe6g28dCuI/8Ibc9Dr9Dr0vJgMTEzGgTf7oEwdhx7xT0Ii5V8oiYiofql2JFR0dHS1Drhy5coHKqg+YCQU2YrTv5/A6Vc/gvLwSshRDgA46tQFl2JmIPij5+DgVO2/l1IdYSQUiYGRUFRTdRkJxZzSSrApJVujzTqHIy99gqA/lqIJSgAApx3a4XTkNCgXj4HcTS5yhfaLTSmJgU0p1VS9yCklooZL0d0boXs/guFkPlIGvotLkuZoc/MEHv/mJVxp6oeUp+bj6rmrYpdJRER2jE0pkR0xC+KP+ASF0pYM4icionqBTSmRHXJt4Yr+Gyejue5kpUH8KUFqBvETEZFVsSklsmNVBfGHZn1iCuI/tfWY2GUSEZEdYFNKRJUE8T9uCuL3GfQo9rSOxNF12WKXSURENoxNKRGZ3Ani324K4pfBiN4F36HjiG4M4iciojrDppSIKhXwUm8EF/2EP9fvx+42I2GAFD0vJqPrxMdxwK0v9r6zGYKRiXJERFQ76kVTmpiYCF9fXzg5OUGpVCIjI6PKucuWLUO/fv3QtGlTNG3aFCqVqsL8sWPHmr4W9fYWHh5e16dBZJM6PNsFfU6twRnNn9jR6SWUwRFdru5Gz9lP4U/Xbtjz2loYyg1il0lERA2c6E3punXroFarMWvWLGRlZaFr164ICwvD+fPnK52fkpKCkSNHYtu2bUhNTYWPjw8GDRqEs2fPms0LDw9HYWGhafv222+tcTpENqvNwHZ4/NBSXMnMQ0qPN3AVjdHx+n70XjgSZ1w7YseoL1Gm59cNExFRzYj+jU5KpRI9e/bEokWLAABGoxE+Pj6YOHEiZsyYcd/9DQYDmjZtikWLFiEqKgrArXdKi4uLsWnTpmrVUFZWhrKyO79M9Xo9fHx8+I1ORPdw5cRl7H8xEQHbPkNz4Va2aaHUG0efVKPHspfQWNFY5AobBn6jE4mB3+hENWWz3+hUXl6OzMxMqFQq05hUKoVKpUJqamq1jnHt2jXcuHEDzZo1MxtPSUlBixYt0LFjR8TGxuLSpaoDwRMSEuDu7m7afHx8anZCRHakabtmFYL4vYznEPrzG7jh3ZpB/EREZBFRm9KLFy/CYDDA09PTbNzT0xNarbZax5g+fTq8vb3NGtvw8HCsXr0aGo0Gc+fOxfbt2zF48GAYDJV/7i0uLg46nc60FRQU1PykiOzM7SD+ZldOYOeYr5DX6BFTEL9jhza3gvj/OHv/AxERkV0T/TOlD2LOnDlYu3YtNm7cCCcnJ9P4iBEjMGzYMAQEBCAiIgI///wz9u7di5SUlEqPI5fL4ebmZrYRkWXkbnL0WzUOrUsOI3XKdzjiHIjGKL0VxN/TDzseHc8gfiIiqpKoTamHhwdkMhmKiorMxouKiqBQKO657/z58zFnzhxs2bIFXbp0uefctm3bwsPDA8ePH3/gmono3m4H8XcsycIf7/1qCuJ//OhXDOInIqIqidqUOjo6IigoCBqNxjRmNBqh0WgQEhJS5X7z5s3De++9h+TkZPTo0eO+r3PmzBlcunQJXl5etVI3Ed2fRCpBj7fDqw7ib/Ekg/iJiMhE9Mv3arUay5YtQ1JSEg4fPozY2FiUlpYiOjoaABAVFYW4uDjT/Llz52LmzJlYsWIFfH19odVqodVqUVJSAgAoKSnB1KlTkZaWhlOnTkGj0WD48OFo3749wsLCRDlHIntXaRD/hV/vBPHP/oVB/EREdk70pjQyMhLz589HfHw8AgMDkZ2djeTkZNPNT/n5+SgsLDTNX7JkCcrLy/Hss8/Cy8vLtM2fPx8AIJPJcODAAQwbNgwdOnTAuHHjEBQUhJ07d0Iul4tyjkR0iymI//8dxY5HX7wTxP/OEAbxExHZOdFzSusjMXIDieyRNuscjrz0CXr8sQSNUQoAOO3QDqdHTodyURTkbrb/F0nmlJIYmFNKNWWzOaVEZN8U3b0Ruvcj3Diej5QBs3FZ0gxtbp7A4/99EZebtkXK0I9Roi0Ru0wiIrICNqVEJLqm7Zoh9Pd4OJ47jZThCyoG8Ye+wyB+IiIbx6aUiOqNxorGCN00pWIQ//bZDOInIrJxbEqJqN75ZxD/nsnrGMRPRGQH2JQSUb0lc5Sh9yfPM4ifiMgOsCklonrvn0H8BxbvQkaLIQziJyKyMWxKiahB6RLbB8FFP+PoumzsaT3CLIh/v3s/BvETETVQbEqJqEHq+HxX9D79rVkQf1f9LgbxExE1UGxKiahBa/NEezx++AtcycxDStDrKIErOl7fj94LR6Kg8aPYEbUMZfoyscskIqL7YFNKRDZB0d0boX/MNwvi971x/E4Q//AFDOInIqrH2JQSkU2pMoj/x9dR7t2GQfxERPUUm1Iiskl3B/GfatQezYTLd4L4e7zOIH4ionqETSkR2bTbQfw+JUfMg/gzFzCIn4ioHmFTSkR24e4g/v1u/e4K4h/BIH4iIhGxKSUiu3I7iL+rbsddQfzrGMRPRCQiNqVEZLcYxE9EVH+wKSUiu3ffIP5J6xjET0RUx9iUEhH9rcog/s9HMIifiKiOsSklIrrL7SD+8j9P38o1rWYQv6HcgOxPU7Bn4rfI/jSF764SEVmgXjSliYmJ8PX1hZOTE5RKJTIyMu45f/369Xj00Ufh5OSEgIAA/PLLL2bPC4KA+Ph4eHl5wdnZGSqVCseOMfKFiCzT7JHmCN02q+og/gGzceXEZQBA2rQNKHLxReCUAei96D8InDIARS6+SJu2QeSzICJqGERvStetWwe1Wo1Zs2YhKysLXbt2RVhYGM6fP1/p/D179mDkyJEYN24c9u3bh4iICERERCA3N9c0Z968efj888+xdOlSpKenw9XVFWFhYbh+/bq1TouIbIhZEH/UsjtB/CnvoFH71khXDEPwR89CYThjtp/CcBbBHz3LxpSIqBokgiCIemupUqlEz549sWjRIgCA0WiEj48PJk6ciBkzZlSYHxkZidLSUvz888+msV69eiEwMBBLly6FIAjw9vbG66+/jjfeeAMAoNPp4OnpiVWrVmHEiBH3rUmv18Pd3R06nQ5ubm61dKZEZCsM5QakT/0ezb9MQMfr+wEAAgBJJXONkKBQ1gqKa3mQOcoqPC/GelPT15RUdoLUIFn7Nz9/dmyHpT87lqw3or5TWl5ejszMTKhUKtOYVCqFSqVCampqpfukpqaazQeAsLAw0/y8vDxotVqzOe7u7lAqlVUes6ysDHq93mwjIqqKzFGG3p9FokPpPqSEzwFQeUMKAFIIaGkoQM5iZp8SEd2LqE3pxYsXYTAY4OnpaTbu6ekJrVZb6T5arfae82//ackxExIS4O7ubtp8fHxqdD5EZF8kUgkc27eu1txrJwrruBoiooZN9M+U1gdxcXHQ6XSmraCgQOySiKiBcGnnVavziIjslahNqYeHB2QyGYqKiszGi4qKoFAoKt1HoVDcc/7tPy05plwuh5ubm9lGRFQdARP64ZysFYxVXMA3QoKzMh8ETOhn5cqIiBoWUZtSR0dHBAUFQaPRmMaMRiM0Gg1CQkIq3SckJMRsPgBs3brVNN/Pzw8KhcJsjl6vR3p6epXHJCKqKZmjDPnqzwCgQmN6+3GB+tNKb3IiIqI7RL98r1arsWzZMiQlJeHw4cOIjY1FaWkpoqOjAQBRUVGIi4szzZ80aRKSk5Px8ccf48iRI3jnnXfwxx9/4NVXXwUASCQSTJ48Ge+//z5+/PFH5OTkICoqCt7e3oiIiBDjFInIxvWa9zQypn4Prayl2XihrBUypn6PXvOeFqkyIqKGw0HsAiIjI3HhwgXEx8dDq9UiMDAQycnJphuV8vPzIZXe6Z179+6NNWvW4O2338abb76JRx55BJs2bYK/v79pzrRp01BaWooXX3wRxcXF6Nu3L5KTk+Hk5FStmm6nZPEufCKqrs5vq2CYdgC7vtqDv/K0cPZT4LGY3ujsKLvnWnL7OWum89V0jdPp6qIaEoO1f73xZ8d2WPqzY8kaJ3pOaX105swZ3oFPRFZVUFCAVq1aWeW1uMYRkbVVZ41jU1oJo9GIc+fOoUmTJpBUM/FXr9fDx8cHBQUFNnGjlC2djy2dC8Dzqe8sPR9BEHD16lV4e3ubXRWqSzVZ4+yBrf0sknXx56dylqxxol++r4+kUmmN37Gwtbv3bel8bOlcAJ5PfWfJ+bi7u9dxNeYeZI2zB7b2s0jWxZ+fiqq7xol+oxMREREREZtSIiIiIhIdm9JaIpfLMWvWLMjlcrFLqRW2dD62dC4Az6e+s7XzsSf8b0cPgj8/D443OhERERGR6PhOKRERERGJjk0pEREREYmOTSkRERERiY5NKRERERGJjk0pEREREYmOTWktSExMhK+vL5ycnKBUKpGRkSF2STWSkJCAnj17okmTJmjRogUiIiJw9OhRscuqNXPmzIFEIsHkyZPFLqXGzp49i1GjRqF58+ZwdnZGQEAA/vjjD7HLspjBYMDMmTPh5+cHZ2dntGvXDu+99x4aShjIjh07MHToUHh7e0MikWDTpk1mzwuCgPj4eHh5ecHZ2RkqlQrHjh0Tp1iqFQ3lZ5OoIWNT+oDWrVsHtVqNWbNmISsrC127dkVYWBjOnz8vdmkW2759O1555RWkpaVh69atuHHjBgYNGoTS0lKxS3tge/fuxRdffIEuXbqIXUqNXblyBX369EGjRo3w66+/4tChQ/j444/RtGlTsUuz2Ny5c7FkyRIsWrQIhw8fxty5czFv3jwsXLhQ7NKqpbS0FF27dkViYmKlz8+bNw+ff/45li5divT0dLi6uiIsLAzXr1+3cqX0oC5cuACj0QiJRCJ2KUS2T6AHEhwcLLzyyiumxwaDQfD29hYSEhJErKp2nD9/XgAgbN++XexSHsjVq1eFRx55RNi6davQv39/YdKkSWKXVCPTp08X+vbtK3YZtWLIkCHC//3f/5mNPf3008ILL7wgUkU1B0DYuHGj6bHRaBQUCoXw0UcfmcaKi4sFuVwufPvttyJUSDW1b98+oVOnTsKOHTvELoXILvCd0gdQXl6OzMxMqFQq05hUKoVKpUJqaqqIldUOnU4HAGjWrJnIlTyYV155BUOGDDH779QQ/fjjj+jRoweee+45tGjRAt26dcOyZcvELqtGevfuDY1Ggz///BMAsH//fuzatQuDBw8WubIHl5eXB61Wa/bz5u7uDqVSaRPrgr3Yv38/evXqhWHDhqFfv35il0MNlPD3xz5ycnKwbt06/PTTT8jLyxO5qvrLQewCGrKLFy/CYDDA09PTbNzT0xNHjhwRqaraYTQaMXnyZPTp0wf+/v5il1Nja9euRVZWFvbu3St2KQ/s5MmTWLJkCdRqNd58803s3bsXr732GhwdHTFmzBixy7PIjBkzoNfr8eijj0Imk8FgMOCDDz7ACy+8IHZpD0yr1QJApevC7eeofjtw4AD69OmDN954A++//75p/Pr163BychKxMmpIDAYDZDIZNmzYgMmTJ8Pd3R2urq4oKyvDV199haCgILFLrHf4TilV6pVXXkFubi7Wrl0rdik1VlBQgEmTJuGbb76xiV8kRqMR3bt3x4cffohu3brhxRdfxPjx47F06VKxS7PYd999h2+++QZr1qxBVlYWkpKSMH/+fCQlJYldGtm5wsJCBAYG4vnnnzdrSGfPno133nkHN2/eFLE6agj0ej0AQCaTYdu2bRg/fjzefPNN5OTkYObMmcjNzcWTTz6J3bt3i1xp/cOm9AF4eHhAJpOhqKjIbLyoqAgKhUKkqh7cq6++ip9//hnbtm1Dq1atxC6nxjIzM3H+/Hl0794dDg4OcHBwwPbt2/H555/DwcEBBoNB7BIt4uXlhc6dO5uNderUCfn5+SJVVHNTp07FjBkzMGLECAQEBGD06NGYMmUKEhISxC7tgd3+f9/W1gV74eLigt69eyM1NRW5ubkAbt249vHHH+Pxxx+HgwMvMFLVsrKy0KxZMxw6dAjXrl3D//73P0yYMAEvv/wyzp07hwkTJuCZZ55Br169EBERgczMTLFLrlfYlD4AR0dHBAUFQaPRmMaMRiM0Gg1CQkJErKxmBEHAq6++io0bN+L333+Hn5+f2CU9kCeeeAI5OTnIzs42bT169MALL7yA7OxsyGQysUu0SJ8+fSpEdP35559o06aNSBXV3LVr1yCVmi8/MpkMRqNRpIpqj5+fHxQKhdm6oNfrkZ6e3iDXBXvj7u6OX3/9FV5eXnjmmWcwefJkfPTRR9iwYQOefPJJscujemz//v0YMGAAJk+ejM6dO8PFxQWjR4/GoEGDUFxcjGHDhiE8PBxr165FdHQ0Ll26hODgYH7W/J/EvtOqoVu7dq0gl8uFVatWCYcOHRJefPFF4aGHHhK0Wq3YpVksNjZWcHd3F1JSUoTCwkLTdu3aNbFLqzUN+e77jIwMwcHBQfjggw+EY8eOCd98843g4uIifP3112KXZrExY8YILVu2FH7++WchLy9P2LBhg+Dh4SFMmzZN7NKq5erVq8K+ffuEffv2CQCEBQsWCPv27RNOnz4tCIIgzJkzR3jooYeEH374QThw4IAwfPhwwc/PT/jrr79Erpwqc+HCBSEtLU3IysoyjV29elV48sknBYlEIixdulTE6qghOHDggODs7CzMnDnTbPz2788tW7YIISEhQl5eniAIgpCWliYMGTJEeO2114QjR45Yu9x6i01pLVi4cKHQunVrwdHRUQgODhbS0tLELqlGAFS6rVy5UuzSak1DbkoFQRB++uknwd/fX5DL5cKjjz4qfPnll2KXVCN6vV6YNGmS0Lp1a8HJyUlo27at8NZbbwllZWVil1Yt27Ztq/T/lTFjxgiCcCsWaubMmYKnp6cgl8uFJ554Qjh69Ki4RVOlDh06JPTv31946qmnhKioKLPndDqd8K9//Uto27atsH//fpEqpPouPz9fePjhh4Xhw4ebjS9YsECYMmWKYDAYhLVr1wpSqVQ4ePCgIAiC8OabbwojR44Url69KkLF9ZdEEPg1FUREZH9ycnIQGhqKCRMmYPz48WjdujUAIDs7GwqFAgqFAiUlJRg6dChOnTqFH3/8EQEBASJXTfXNgQMHEBUVhbZt2yImJgZPPvkkPvroI7zzzjv46aefMHDgQOTn52PChAnYvXs3unbtij/++AOpqan8eboLm1IiIrI7586dw6BBgzBo0CAsWLDAND537lzExcVhxowZeP3119G8eXOUlJQgIiICe/fuRWpqaoUbDsk+6fV6ODo6wsnJCRkZGZg2bRqaN2+Oxo0b45dffsG6deswcOBA0/x9+/bh999/x+XLlxEVFYWOHTuKWH39xNsIiYjI7mRmZsLJyQkvvfQSjEYjpFIp5s+fj/feew+TJk3CvHnzIJFIoFar0bx5c2zcuBEvvPACHB0dxS6d6oHCwkJERUVh+PDhGDduHIKDgzF37lxMnz4dycnJmDFjhqkhvXnzJhwcHNCtWzd069YNgiDwa2urwKaUiIjszvbt23H58mXTu1U3b96EQqHApk2boFKpEBwcjBdeeAHl5eWYPXs2mjRpgh9++IHNBAG49U2HMpkMa9asgZOTE1544QUolUp88sknUKvV2Lt3LzZv3owhQ4bAwcHB9Bcfujf+GyIiIrtw7NgxU1RXkyZNIJVKceHCBRgMBjg4OOCFF16ASqWCIAgYOXIkxo0bh7S0NFN8HBtSAoAbN25ALpfjhx9+QKtWrfDll1/im2++wV9//YVu3bph7ty5uHr1KhYvXoxff/0VAMwaUv4cVY1NKRER2bzs7Gx0797dlPUbEhKCkydPYtOmTZVmFt+4cQMymQxKpZLvcBEA4OrVqwCARo0aAQDkcjlWr16N1q1b44svvjA1prcv5ZeXl+PDDz/Eli1bxCy7QeH/aUREZNP279+PPn364NVXX8WECRMAAKGhoRg7dixeeuklfPvttwDuvINlNBoxa9Ys/Pjjj3jxxRdNTQjZr8OHD6NVq1YYMWIE3nzzTZw8eRLnzp2Dk5MTvvnmGzz66KNYvHgxvv76a1y7dg3BwcF455138NBDD6FTp05il99g8O57IiKyWQcOHEBISAgmT56MDz74wDS+bds2aLVarFu3Dj/99BNefvll9OnTB5cvX0ZaWhp++eUXaDQadOvWTcTqqT64efMmVq9ejZiYGCgUCvj5+eHkyZPw8PBAeHg4wsPD0bNnT4wdOxZ//fUXnn32WfznP/+Bs7Mzrl+/DicnJ7FPocHgjU5ERGSTCgoK8MQTT+Cpp54ya0jfffddrFy5EhqNBkFBQejevTs+++wzfPPNN/Dy8kJgYCB27drF6CfCwYMHsWHDBkyePBnnz5/HW2+9hYULF8LDwwOHDh3C6tWrsXbtWigUCnh6emL79u04evSo6eYnuVwu9ik0KLx8T0RENslgMMDPzw/Xr1/H7t27AQBz5szBwoULkZiYiLZt26JDhw6Ij4/HwYMHkZ2djd27d2PVqlVsSAn79+9HQEAAHBwc0KRJE8yYMQNqtRqjR4/GuXPnEBsbi23btmH//v14/vnn8dhjj8HR0RHXrl1Dr169APCmJkvx8j0REdmsY8eO4bXXXoOjoyM8PT2xadMmfP311xg0aJBZXuShQ4fYiJLJoUOH0KNHD0ybNg3vvPOO2XPTp0/HJ598ghUrVmDUqFFmz508eRJNmjTBww8/bMVqbQebUiIisml//vknXn31VezatQvvvfceXn/9ddz+1SeRSDBz5kysWrUKOTk5cHd357tbdi43NxcDBgzAww8/jEOHDgG4lcbwzxveZsyYgQULFiApKQkjR44Uq1Sbw8v3RERk0zp06IAlS5agX79+0Gg02LlzJyQSCSQSCeLj4zF//nxs2rQJDz30EBtSO7d//34olUr4+/tDp9Nh0qRJAG7FQBkMBtO8OXPmQK1WY/z48Vi5cqVY5docvlNKRER24falfEEQkJCQgK1bt2LWrFnYtWsXgoKCxC6PRPbHH3+gd+/eeOutt/D2229j+fLleOutt/Cf//wHn332GYBbn1P+Z67tq6++ivXr1+PYsWNwc3MTq3SbwaaUiIjsxrFjx6BWq5GRkYErV64gNTWVDSkBAHbs2IH//e9/pgZUp9Nh3bp1921Mz58/jxYtWohSs61hU0pERHbl6NGjmDZtGj788EM89thjYpdD9dDtm+D0ej3Wrl1boTG9efMmHByYqlnb2JQSEZHdufvGFaKq/LMxHT16NBYsWCB2STaLbT4REdkdNqRUXW5ubhgxYgSkUilefPFFyOVyJCQkiF2WTWJTSkRERHQPbm5ueO6559CoUSOEhISIXY7N4uV7IiIiomr45xcuUO1jTikRERFRNbAhrVtsSomIiIhIdGxKiYiIiEh0bEqJiIiISHRsSomIiIhIdGxKiYiIiEh0bEqJiIiISHRsSomIiIhIdGxKiYiIiEh0bEqJiIiISHRsSomIiIhIdGxKiYiIiEh0/x/NzDmj++GT8AAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_fitness(G2_prep, exp_list_G2_egf, P2, measured_only=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G2_prep.plot(custom_edge_attr=cno.cno_style(P2, flow_name=\"edge_activates\", scale=None, iexp=0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inhibition with AND gates" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Please remember, inhibitions are added to the incoming edge of the AND node. The outgoing edges of the AND node are always activating by convention." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "# Test graph\n", - "G2 = cn.Graph.from_sif_tuples(\n", - " [\n", - " (\"EGF\", -1, \"AND1\"),\n", - " (\"TNFa\", 1, \"AND1\"),\n", - " (\"AND1\", 1, \"Ras\"),\n", - " (\"EGF\", -1, \"Ras\"),\n", - " (\"TNFa\", 1, \"Ras\"),\n", - " ]\n", - ")\n", - "\n", - "# RAS is only active iff both EGF and TNFa are active -> we need to idetify the AND gate\n", - "exp_list_G2_and = {\n", - " \"exp0\": {\"input\": {\"EGF\": 0, \"TNFa\": 0}, \"output\": {\"Ras\": 0}},\n", - " \"exp1\": {\"input\": {\"EGF\": 1, \"TNFa\": 0}, \"output\": {\"Ras\": 0}},\n", - " \"exp2\": {\"input\": {\"EGF\": 0, \"TNFa\": 1}, \"output\": {\"Ras\": 1}},\n", - " \"exp3\": {\"input\": {\"EGF\": 1, \"TNFa\": 1}, \"output\": {\"Ras\": 0}},\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G2_prep = cno.expand_graph_for_flows(G2, exp_list_G2_and)\n", - "G2_prep.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "AND1\n", - "\n", - "AND1\n", - "\n", - "\n", - "\n", - "EGF->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "AND1->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->AND1\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "Ras->e_5_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source\n", - "\n", - "\n", - "\n", - "\n", - "e_6_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source\n", - "\n", - "\n", - "\n", - "\n", - "e_7_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "P2 = cno.cellnoptILP(G2_prep, exp_list_G2_and, verbose=False)\n", - "G2_prep.plot(custom_edge_attr=cno.cno_style(P2, flow_name=\"edge_activates\", scale=None, iexp=2))" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAqUAAAIYCAYAAABUjJWZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmrUlEQVR4nO3de1xVZfr38S9gbNSEVEa2KClpmY4ghUJYphU/sczRXyd0MtQntawcdZcHmsTMCjVzrDxmmvh00Jwxa8YyfRjRNJQ8aypZauBh4xG2YoJu1vOH4552orIRWAif9+u1Xrnuda97X8t2975ah2t5GYZhCAAAADCRt9kBAAAAACSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdCSlAAAAMB1JKQAAAExHUgoAAADTkZQCAADAdCSlAAAAMB1JKXAZ8+bNk5eXl2upUaOGGjVqpL59++rgwYNmhwcAHvn9nObn56fbbrtNL7zwgnJycsrsc6ZPn6558+aV2XioPmqYHQBQ2b322msKDQ3V2bNntW7dOs2bN09r1qzRjh075OfnZ3Z4AOCR385pa9as0YwZM/TVV19px44dqlWr1jWPP336dAUGBqpv377XHiyqFZJS4CoefPBBtW3bVpLUv39/BQYGasKECfryyy/1xBNPmBwdAHjm93Na/fr1NXnyZH3xxRfq1atXqcc9c+ZMmSS1qL64fA94qEOHDpKkn3/+WZJUWFiopKQkRUZGKiAgQLVr11aHDh20cuXKS/ZdsGCBIiMjVadOHfn7+yssLEzvvPNOhcYPAL91//33S5L27dsnSfroo48UGRmpmjVrql69eurZs6eys7Pd9unUqZNat26tjRs36t5771WtWrX08ssvq2nTpvrhhx+0atUq120CnTp1quhDwnWKM6WAh/bv3y9Jqlu3riTJ4XDogw8+UK9evTRgwACdOnVKc+bMUVxcnDIyMhQRESFJWrFihXr16qUHHnhAEyZMkCTt2rVLa9eu1ZAhQ8w4FABw/Q92/fr19cYbb2j06NF64okn1L9/fx09elTvvfee7r33Xm3evFk33XSTa7/jx4/rwQcfVM+ePdW7d28FBQWpU6dOGjx4sG688Ub99a9/lSQFBQWZcVi4DpGUAleRl5enY8eO6ezZs1q/fr3Gjh0ri8Wihx9+WNKF5HT//v3y9fV17TNgwADdfvvteu+99zRnzhxJ0tKlS+Xv769vvvlGPj4+phwLAPx2Tlu7dq1ee+011axZU126dFGzZs30+uuv6+WXX3b1f+SRR3THHXdo+vTpbu12u10zZ87UM8884zb+K6+8osDAQPXu3bvCjglVA0kpcBWxsbFu602bNtVHH32kxo0bS5J8fHxcSWZRUZFyc3NVVFSktm3batOmTa79brrpJuXn52vFihXq0qVLxR0AAPzG7+e0Jk2a6OOPP9bixYtVVFSkJ554QseOHXNtt1qtuvXWW7Vy5Uq3pNRisahfv34VFjeqPpJS4CqmTZum2267TXl5eZo7d65Wr14ti8Xi1iclJUVvv/22du/erXPnzrnaQ0NDXX9+7rnn9Nlnn+nBBx9Uo0aN1LlzZz3xxBMkqAAq1MU5rUaNGgoKClKLFi3k7e2tL774QoZh6NZbby12vxtuuMFtvVGjRm5XiIBrRVIKXEVUVJTrSdUePXronnvu0Z///GdlZmbqxhtv1EcffaS+ffuqR48eGj58uBo0aCAfHx8lJye77tWSpAYNGmjLli365ptv9PXXX+vrr7/Whx9+qISEBKWkpJh1eACqmd/Oab9VVFQkLy8vff3118XeYnTjjTe6rdesWbPcYkT1RFIKeOBisnnfffdp6tSpGjVqlP7+97/rlltu0eLFi+Xl5eXqO2bMmEv29/X1Vbdu3dStWzcVFRXpueee06xZszR69Gg1b968Ig8FANw0a9ZMhmEoNDRUt912W6nH+e08CHiCklCAhzp16qSoqChNmTJFZ8+edZ1RMAzD1Wf9+vVKT0932+/48eNu697e3goPD5ckFRQUlHPUAHBljzzyiHx8fDR27Fi3+Uy6ML/9fg67nNq1ays3N/eS9nPnzmn37t06fPhwWYSLKogzpUApDB8+XI8//rjmzZunhx9+WIsXL9b//u//qmvXrtq3b59mzpypVq1a6fTp0659+vfvrxMnTuj+++9X48aN9csvv+i9995TRESEWrZsaeLRAIBcT94nJiZq//796tGjh+rUqaN9+/bp888/18CBA/XSSy9ddZzIyEjNmDFDr7/+upo3b64GDRro/vvv18GDB9WyZUv16dOH15CiWCSlQCk88sgjatasmSZNmqTMzEzZ7XbNmjVL33zzjVq1aqWPPvpIixYtUlpammuf3r176/3339f06dOVm5sr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klIAAACYjqQUAAAApiMpLQPTpk1T06ZN5efnp+joaGVkZJgdUqkkJyerXbt2qlOnjho0aKAePXooMzPT7LDKzPjx4+Xl5aWhQ4eaHUqpHTx4UL1791b9+vVVs2ZNhYWFacOGDWaH5TGn06nRo0crNDRUNWvWVLNmzTRu3DhdL8VAVq9erW7duik4OFheXl5asmSJ23bDMJSUlKSGDRuqZs2aio2N1Z49e8wJFmXievluAtczktJrtHDhQtlsNo0ZM0abNm1SmzZtFBcXpyNHjpgdmsdWrVql559/XuvWrdOKFSt07tw5de7cWfn5+WaHds2+//57zZo1S+Hh4WaHUmonT57U3XffrRtuuEFff/21du7cqbffflt169Y1OzSPTZgwQTNmzNDUqVO1a9cuTZgwQRMnTtR7771ndmglkp+frzZt2mjatGnFbp84caLeffddzZw5U+vXr1ft2rUVFxens2fPVnCkuFZHjx5VUVGRvLy8zA4FqPoMXJOoqCjj+eefd607nU4jODjYSE5ONjGqsnHkyBFDkrFq1SqzQ7kmp06dMm699VZjxYoVRseOHY0hQ4aYHVKpjBw50rjnnnvMDqNMdO3a1fg//+f/uLU98sgjxpNPPmlSRKUnyfj8889d60VFRYbVajXeeustV1tubq5hsViMTz/91IQIUVqbN282WrZsaaxevdrsUIBqgTOl16CwsFAbN25UbGysq83b21uxsbFKT083MbKykZeXJ0mqV6+eyZFcm+eff15du3Z1+/d0Pfryyy/Vtm1bPf7442rQoIHuuOMOzZ492+ywSqV9+/ZKTU3Vjz/+KEnaunWr1qxZowcffNDkyK7dvn37ZLfb3b5vAQEBio6OrhLzQnWxdetW3XXXXfrTn/6kDh06mB0OrlPGf2772L59uxYuXKh//vOf2rdvn8lRVV41zA7genbs2DE5nU4FBQW5tQcFBWn37t0mRVU2ioqKNHToUN19991q3bq12eGU2oIFC7Rp0yZ9//33Zodyzfbu3asZM2bIZrPp5Zdf1vfff6+//OUv8vX1VZ8+fcwOzyOjRo2Sw+HQ7bffLh8fHzmdTr3xxht68sknzQ7tmtntdkkqdl64uA2V27Zt23T33XfrpZde0uuvv+5qP3v2rPz8/EyMDNcTp9MpHx8fLV68WEOHDlVAQIBq166tgoICffDBB4qMjDQ7xEqHM6Uo1vPPP68dO3ZowYIFZodSatnZ2RoyZIg+/vjjKvFDUlRUpDvvvFNvvvmm7rjjDg0cOFADBgzQzJkzzQ7NY5999pk+/vhjffLJJ9q0aZNSUlI0adIkpaSkmB0aqrnDhw8rIiJCTzzxhFtCOnbsWL366qs6f/68idHheuBwOCRJPj4+WrlypQYMGKCXX35Z27dv1+jRo7Vjxw499NBDWrt2rcmRVj4kpdcgMDBQPj4+ysnJcWvPycmR1Wo1Kapr98ILL+hf//qXVq5cqcaNG5sdTqlt3LhRR44c0Z133qkaNWqoRo0aWrVqld59913VqFFDTqfT7BA90rBhQ7Vq1cqtrWXLlsrKyjIpotIbPny4Ro0apZ49eyosLExPPfWUhg0bpuTkZLNDu2YX/9uvavNCdVGrVi21b99e6enp2rFjh6QLD669/fbbuvfee1WjBhcYcXmbNm1SvXr1tHPnTp05c0b/+Mc/9Nxzz+nZZ5/VoUOH9Nxzz+nRRx/VXXfdpR49emjjxo1mh1ypkJReA19fX0VGRio1NdXVVlRUpNTUVMXExJgYWekYhqEXXnhBn3/+uf79738rNDTU7JCuyQMPPKDt27dry5YtrqVt27Z68skntWXLFvn4+JgdokfuvvvuS0p0/fjjj2rSpIlJEZXemTNn5O3tPv34+PioqKjIpIjKTmhoqKxWq9u84HA4tH79+utyXqhuAgIC9PXXX6thw4Z69NFHNXToUL311ltavHixHnroIbPDQyW2detW3XfffRo6dKhatWqlWrVq6amnnlLnzp2Vm5urP/3pT+rSpYsWLFigfv366fjx44qKiuJe898y+0mr692CBQsMi8VizJs3z9i5c6cxcOBA46abbjLsdrvZoXls0KBBRkBAgJGWlmYcPnzYtZw5c8bs0MrM9fz0fUZGhlGjRg3jjTfeMPbs2WN8/PHHRq1atYyPPvrI7NA81qdPH6NRo0bGv/71L2Pfvn3G4sWLjcDAQGPEiBFmh1Yip06dMjZv3mxs3rzZkGRMnjzZ2Lx5s/HLL78YhmEY48ePN2666Sbjiy++MLZt22Z0797dCA0NNX799VeTI0dxjh49aqxbt87YtGmTq+3UqVPGQw89ZHh5eRkzZ840MTpcD7Zt22bUrFnTGD16tFv7xd/P5cuXGzExMca+ffsMwzCMdevWGV27djX+8pe/GLt3767ocCstktIy8N577xk333yz4evra0RFRRnr1q0zO6RSkVTs8uGHH5odWpm5npNSwzCMf/7zn0br1q0Ni8Vi3H777cb7779vdkil4nA4jCFDhhg333yz4efnZ9xyyy3GX//6V6OgoMDs0Epk5cqVxf630qdPH8MwLpSFGj16tBEUFGRYLBbjgQceMDIzM80NGsXauXOn0bFjR+Phhx82EhIS3Lbl5eUZ//M//2PccsstxtatW02KEJVdVlaW8Yc//MHo3r27W/vkyZONYcOGGU6n01iwYIHh7e1t/PDDD4ZhGMbLL79s9OrVyzh16pQJEVdeXobBayoAANXP9u3b1alTJz333HMaMGCAbr75ZknSli1bZLVaZbVadfr0aXXr1k379+/Xl19+qbCwMJOjRmWzbds2JSQk6JZbblH//v310EMP6a233tKrr76qf/7zn7r//vuVlZWl5557TmvXrlWbNm20YcMGpaen8336HZJSAEC1c+jQIXXu3FmdO3fW5MmTXe0TJkxQYmKiRo0apRdffFH169fX6dOn1aNHD33//fdKT0+/5IFDVE8Oh0O+vr7y8/NTRkaGRowYofr16+vGG2/UV199pYULF+r+++939d+8ebP+/e9/68SJE0pISFCLFi1MjL5y4jFCAEC1s3HjRvn5+emZZ55RUVGRvL29NWnSJI0bN05DhgzRxIkT5eXlJZvNpvr16+vzzz/Xk08+KV9fX7NDRyVw+PBhJSQkqHv37nr66acVFRWlCRMmaOTIkVq2bJlGjRrlSkjPnz+vGjVq6I477tAdd9whwzB4be1lkJQCAKqdVatW6cSJE66zVefPn5fVatWSJUsUGxurqKgoPfnkkyosLNTYsWNVp04dffHFFyQTkHThTYc+Pj765JNP5OfnpyeffFLR0dH629/+JpvNpu+//15Lly5V165dVaNGDdf/+ODK+BsCAFQLe/bscZXqqlOnjry9vXX06FE5nU7VqFFDTz75pGJjY2UYhnr16qWnn35a69atc5WPIyGFJJ07d04Wi0VffPGFGjdurPfff18ff/yxfv31V91xxx2aMGGCTp06penTp+vrr7+WJLeElO/R5ZGUAgCqvC1btujOO+901fqNiYnR3r17tWTJkmJrFp87d04+Pj6Kjo7mDBckSadOnZIk3XDDDZIki8Wi+fPn6+abb9asWbNcienFS/mFhYV68803tXz5cjPDvq7wXxoAoErbunWr7r77br3wwgt67rnnJEmdOnVS37599cwzz+jTTz+V9N8zWEVFRRozZoy+/PJLDRw40JWEoPratWuXGjdurJ49e+rll1/W3r17dejQIfn5+enjjz/W7bffrunTp+ujjz7SmTNnFBUVpVdffVU33XSTWrZsaXb41w2evgcAVFnbtm1TTEyMhg4dqjfeeMPVvnLlStntdi1cuFD//Oc/9eyzz+ruu+/WiRMntG7dOn311VdKTU3VHXfcYWL0qAzOnz+v+fPnq3///rJarQoNDdXevXsVGBioLl26qEuXLmrXrp369u2rX3/9VY899pj+/Oc/q2bNmjp79qz8/PzMPoTrBg86AQCqpOzsbD3wwAN6+OGH3RLS1157TR9++KFSU1MVGRmpO++8U++8844+/vhjNWzYUBEREVqzZg2ln6AffvhBixcv1tChQ3XkyBH99a9/1XvvvafAwEDt3LlT8+fP14IFC2S1WhUUFKRVq1YpMzPT9fCTxWIx+xCuK1y+BwBUSU6nU6GhoTp79qzWrl0rSRo/frzee+89TZs2Tbfccotuu+02JSUl6YcfftCWLVu0du1azZs3j4QU2rp1q8LCwlSjRg3VqVNHo0aNks1m01NPPaVDhw5p0KBBWrlypbZu3aonnnhCf/zjH+Xr66szZ87orrvuksRDTZ7i8j0AoMras2eP/vKXv8jX11dBQUFasmSJPvroI3Xu3NmtXuTOnTtJROGyc+dOtW3bViNGjNCrr77qtm3kyJH629/+prlz56p3795u2/bu3as6deroD3/4QwVGW3WQlAIAqrQff/xRL7zwgtasWaNx48bpxRdf1MWfPi8vL40ePVrz5s3T9u3bFRAQwNmtam7Hjh2677779Ic//EE7d+6UdKEaw28feBs1apQmT56slJQU9erVy6xQqxwu3wMAqrTbbrtNM2bMUIcOHZSamqpvv/1WXl5e8vLyUlJSkiZNmqQlS5bopptuIiGt5rZu3aro6Gi1bt1aeXl5GjJkiKQLZaCcTqer3/jx42Wz2TRgwAB9+OGHZoVb5XCmFABQLVy8lG8YhpKTk7VixQqNGTNGa9asUWRkpNnhwWQbNmxQ+/bt9de//lWvvPKK5syZo7/+9a/685//rHfeeUfShfuUf1vX9oUXXtCiRYu0Z88e+fv7mxV6lUFSCgCoNvbs2SObzaaMjAydPHlS6enpJKSQJK1evVr/+Mc/XAloXl6eFi5ceNXE9MiRI2rQoIEpMVc1JKUAgGolMzNTI0aM0Jtvvqk//vGPZoeDSujiQ3AOh0MLFiy4JDE9f/68atSgqmZZIykFAFQ7v39wBbic3yamTz31lCZPnmx2SFUWaT4AoNohIUVJ+fv7q2fPnvL29tbAgQNlsViUnJxsdlhVEkkpAADAFfj7++vxxx/XDTfcoJiYGLPDqbK4fA8AAFACv33hAsoedUoBAABKgIS0fJGUAgAAwHQkpQAAADAdSSkAAABMR1IKAAAA05GUAgAAwHQkpQAAADAdSSkAAABMR1IKAAAA05GUAgAAwHQkpQAAADAdSSkAAABM9/8BqHpoTpEIL8sAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_fitness(G2_prep, exp_list_G2_and, P2, measured_only=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Complex case study with inhibition" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's look at a more realistic case study which contains a larger network and more experiments.\n", - "\n", - "Also note that the we define inhibition in the experimental description. This could be, for example, the effect of small molecule kinase inhibitor. \n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n", - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n" - ] - }, - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "PI3K\n", - "\n", - "PI3K\n", - "\n", - "\n", - "\n", - "EGF->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf\n", - "\n", - "Raf\n", - "\n", - "\n", - "\n", - "Ras->Raf\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Akt\n", - "\n", - "Akt\n", - "\n", - "\n", - "\n", - "PI3K->Akt\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TRAF6\n", - "\n", - "TRAF6\n", - "\n", - "\n", - "\n", - "TNFa->TRAF6\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p38\n", - "\n", - "p38\n", - "\n", - "\n", - "\n", - "TRAF6->p38\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Jnk\n", - "\n", - "Jnk\n", - "\n", - "\n", - "\n", - "TRAF6->Jnk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "NFkB\n", - "\n", - "NFkB\n", - "\n", - "\n", - "\n", - "TRAF6->NFkB\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Hsp27\n", - "\n", - "Hsp27\n", - "\n", - "\n", - "\n", - "p38->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cJun\n", - "\n", - "cJun\n", - "\n", - "\n", - "\n", - "Jnk->cJun\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Mek\n", - "\n", - "Mek\n", - "\n", - "\n", - "\n", - "Akt->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p90RSK\n", - "\n", - "p90RSK\n", - "\n", - "\n", - "\n", - "Mek->p90RSK\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk\n", - "\n", - "Erk\n", - "\n", - "\n", - "\n", - "Mek->Erk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G3 = cn.Graph.from_sif_tuples(\n", - " [\n", - " (\"EGF\", 1, \"Ras\"),\n", - " (\"EGF\", 1, \"PI3K\"),\n", - " (\"TNFa\", 1, \"PI3K\"),\n", - " (\"TNFa\", 1, \"TRAF6\"),\n", - " (\"TRAF6\", 1, \"p38\"),\n", - " (\"TRAF6\", 1, \"Jnk\"),\n", - " (\"TRAF6\", 1, \"NFkB\"),\n", - " (\"Jnk\", 1, \"cJun\"),\n", - " (\"p38\", 1, \"Hsp27\"),\n", - " (\"PI3K\", 1, \"Akt\"),\n", - " (\"Ras\", 1, \"Raf\"),\n", - " (\"Raf\", 1, \"Mek\"),\n", - " (\"Akt\", -1, \"Mek\"),\n", - " (\"Mek\", 1, \"p90RSK\"),\n", - " (\"Mek\", 1, \"Erk\"),\n", - " (\"Erk\", 1, \"Hsp27\"),\n", - " ]\n", - ")\n", - "# G3.add_edge((), 'EGF')\n", - "# G3.add_edge((), 'TNFa')\n", - "# G3.add_edge('Akt', ())\n", - "# G3.add_edge('cJun', ())\n", - "# G3.add_edge('Hsp27', ())\n", - "# G3.add_edge('NFkB', ())\n", - "# G3.add_edge('Erk', ())\n", - "# G3.add_edge('p90RSK', ())\n", - "# G3.add_edge('Jnk', ())\n", - "\n", - "exp_list_toy_full = {\n", - " \"exp0\": {\n", - " \"input\": {\"EGF\": 0, \"TNFa\": 0},\n", - " \"inhibition\": {},\n", - " \"output\": {\n", - " \"Akt\": 0,\n", - " \"Hsp27\": 0,\n", - " \"NFkB\": 0,\n", - " \"Erk\": 0,\n", - " \"p90RSK\": 0,\n", - " \"Jnk\": 0,\n", - " \"cJun\": 0,\n", - " },\n", - " },\n", - " \"exp1\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 0},\n", - " \"inhibition\": {},\n", - " \"output\": {\n", - " \"Akt\": 0.91,\n", - " \"Hsp27\": 0,\n", - " \"NFkB\": 0.86,\n", - " \"Erk\": 0.8,\n", - " \"p90RSK\": 0.88,\n", - " \"Jnk\": 0,\n", - " \"cJun\": 0,\n", - " },\n", - " },\n", - " \"exp2\": {\n", - " \"input\": {\"EGF\": 0, \"TNFa\": 1},\n", - " \"inhibition\": {},\n", - " \"output\": {\n", - " \"Akt\": 0.82,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.0,\n", - " \"p90RSK\": 0.0,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - " \"exp3\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 1},\n", - " \"inhibition\": {},\n", - " \"output\": {\n", - " \"Akt\": 0.91,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.8,\n", - " \"p90RSK\": 0.88,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - " \"exp4\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 0},\n", - " \"inhibition\": {\"Raf\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.91,\n", - " \"Hsp27\": 0,\n", - " \"NFkB\": 0.86,\n", - " \"Erk\": 0.0,\n", - " \"p90RSK\": 0.0,\n", - " \"Jnk\": 0,\n", - " \"cJun\": 0,\n", - " },\n", - " },\n", - " \"exp5\": {\n", - " \"input\": {\"EGF\": 0, \"TNFa\": 1},\n", - " \"inhibition\": {\"Raf\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.82,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.0,\n", - " \"p90RSK\": 0.0,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - " \"exp6\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 1},\n", - " \"inhibition\": {\"Raf\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.91,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.0,\n", - " \"p90RSK\": 0.0,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - " \"exp7\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 0},\n", - " \"inhibition\": {\"PI3K\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.0,\n", - " \"Hsp27\": 0,\n", - " \"NFkB\": 0.0,\n", - " \"Erk\": 0.8,\n", - " \"p90RSK\": 0.88,\n", - " \"Jnk\": 0,\n", - " \"cJun\": 0,\n", - " },\n", - " },\n", - " \"exp8\": {\n", - " \"input\": {\"EGF\": 0, \"TNFa\": 1},\n", - " \"inhibition\": {\"PI3K\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.0,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.0,\n", - " \"p90RSK\": 0.0,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - " \"exp9\": {\n", - " \"input\": {\"EGF\": 1, \"TNFa\": 1},\n", - " \"inhibition\": {\"PI3K\": 1},\n", - " \"output\": {\n", - " \"Akt\": 0.0,\n", - " \"Hsp27\": 0.7,\n", - " \"NFkB\": 0.90,\n", - " \"Erk\": 0.8,\n", - " \"p90RSK\": 0.88,\n", - " \"Jnk\": 0.25,\n", - " \"cJun\": 0.4,\n", - " },\n", - " },\n", - "}\n", - "\n", - "\n", - "G3.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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1w6hoaHo2bOnTpKaO7F0cnKCn59fnmXIDh06hI8++ggdOnTIt/zT2rVr4eLigt69e5vwG1ond3d3nTe00qVL4+nTpzrPdZS/3S+++AI//fQT5syZg0WLFul9TtmyZXXeGPV55513UKFCBfz555/w8PDQ+xwXFxed1+rUqRNq166NyMhIbN682fRfopA9evQIKSkpFivtplK1alWdc3ldr3LIfd+pVasWnJ2dcfv2bbi7u2P79u3o1asXevToAYCSorlz52LmzJla10exYsXUQ/85qear5p4OoPqgpFAocP78ecyYMQNPnz7VWyWiTp06+Omnn6BQKHDx4kX8/vvvmDt3LoYMGYIaNWpoXYt+fn4ICwvDuHHj0KdPH2zcuFGn00Quqs6MIkWKoEKFCqhbty6cnZ2xbds2CCHy7FwoWrSo1rGvr6+kahpy48VPBuzbtw+JiYlYv349ateurX706tULAHQWQXXo0AElSpTA3LlzTa7558gMzYvs1q0bhBCYM2cOlEplIbaqcBUrVgwHDx7Enj170K9fP5w9exa9e/fGW2+9le9iHUP+/fdfdO3aFQEBAdi0aZPBBSdpaWnYunUr2rdvb31zjiQw5U3GUf52a9asib59+2Lp0qVITEws0Gv16NEDN27cMGoxaE6VK1dG3bp1cfDgwQL9fGuXV09qXn/PeV2vQgiztcmccv9+DRo0wPnz53H+/HkcOnQI9+7dw+DBg/H48WOtnvFKlSrpvfZU53LPf1Z9UOrQoQNGjx6NNWvW4JdffsnzgxVAsWzYsCEiIyOxdetWALrv1wAwduxYjB07Flu3bsXgwYOtJtbBwcFo37492rRpA39/f3XnhFKphJOTE3bu3Indu3frPL7//nut1zH03mrNuMfUgLVr16J8+fL49ttvdb62ZcsWbN26FUuWLFGfa9GiBYYOHYouXbrgvffew9atW7USAXMN+TiS7t274+2330b//v3h6emJmJgYuZtkMc7OzmjXrh3atWuH6OhozJo1CxMnTkRsbKzWJ/1r165pfZ8QAtevX0dgYKDW+Rs3buC///0vypcvjx07duTZq6Xy66+/IjU11eYXPUmR39+uPfniiy+wZs0afPnllwV6nXnz5qFIkSLqxToffPCB0d+bnZ2N58+fF+jnF5Zy5crBy8sL58+fN+n7SpcurbOIE6AKBrbo2rVr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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_data(exp_list_toy_full)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n", - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n" - ] - }, - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "PI3K\n", - "\n", - "PI3K\n", - "\n", - "\n", - "\n", - "EGF->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf\n", - "\n", - "Raf\n", - "\n", - "\n", - "\n", - "Ras->Raf\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Akt\n", - "\n", - "Akt\n", - "\n", - "\n", - "\n", - "PI3K->Akt\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TRAF6\n", - "\n", - "TRAF6\n", - "\n", - "\n", - "\n", - "TNFa->TRAF6\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p38\n", - "\n", - "p38\n", - "\n", - "\n", - "\n", - "TRAF6->p38\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Jnk\n", - "\n", - "Jnk\n", - "\n", - "\n", - "\n", - "TRAF6->Jnk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "NFkB\n", - "\n", - "NFkB\n", - "\n", - "\n", - "\n", - "TRAF6->NFkB\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Hsp27\n", - "\n", - "Hsp27\n", - "\n", - "\n", - "\n", - "p38->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cJun\n", - "\n", - "cJun\n", - "\n", - "\n", - "\n", - "Jnk->cJun\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "Jnk->e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "NFkB->e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "cJun->e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_16_target\n", - "\n", - "\n", - "\n", - "\n", - "Hsp27->e_16_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Mek\n", - "\n", - "Mek\n", - "\n", - "\n", - "\n", - "Akt->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "Akt->e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p90RSK\n", - "\n", - "p90RSK\n", - "\n", - "\n", - "\n", - "Mek->p90RSK\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk\n", - "\n", - "Erk\n", - "\n", - "\n", - "\n", - "Mek->Erk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_target\n", - "\n", - "\n", - "\n", - "\n", - "p90RSK->e_17_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_target\n", - "\n", - "\n", - "\n", - "\n", - "Erk->e_18_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_23_source\n", - "\n", - "\n", - "\n", - "\n", - "e_23_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_24_source\n", - "\n", - "\n", - "\n", - "\n", - "e_24_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G3_prep = cno.expand_graph_for_flows(G3, exp_list_toy_full)\n", - "G3_prep.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "P_G3 = cno.cellnoptILP(G3_prep, exp_list_toy_full, verbose=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n", - "Warning: node 'TRAF6', graph '%3' size too small for label\n", - "Warning: node 'p90RSK', graph '%3' size too small for label\n" - ] - }, - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "EGF\n", - "\n", - "EGF\n", - "\n", - "\n", - "\n", - "Ras\n", - "\n", - "Ras\n", - "\n", - "\n", - "\n", - "EGF->Ras\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "PI3K\n", - "\n", - "PI3K\n", - "\n", - "\n", - "\n", - "EGF->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf\n", - "\n", - "Raf\n", - "\n", - "\n", - "\n", - "Ras->Raf\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Akt\n", - "\n", - "Akt\n", - "\n", - "\n", - "\n", - "PI3K->Akt\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TNFa\n", - "\n", - "TNFa\n", - "\n", - "\n", - "\n", - "TNFa->PI3K\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "TRAF6\n", - "\n", - "TRAF6\n", - "\n", - "\n", - "\n", - "TNFa->TRAF6\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p38\n", - "\n", - "p38\n", - "\n", - "\n", - "\n", - "TRAF6->p38\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Jnk\n", - "\n", - "Jnk\n", - "\n", - "\n", - "\n", - "TRAF6->Jnk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "NFkB\n", - "\n", - "NFkB\n", - "\n", - "\n", - "\n", - "TRAF6->NFkB\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Hsp27\n", - "\n", - "Hsp27\n", - "\n", - "\n", - "\n", - "p38->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "cJun\n", - "\n", - "cJun\n", - "\n", - "\n", - "\n", - "Jnk->cJun\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "Jnk->e_22_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "NFkB->e_20_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "cJun->e_19_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_16_target\n", - "\n", - "\n", - "\n", - "\n", - "Hsp27->e_16_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Mek\n", - "\n", - "Mek\n", - "\n", - "\n", - "\n", - "Akt->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "Akt->e_21_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Raf->Mek\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "p90RSK\n", - "\n", - "p90RSK\n", - "\n", - "\n", - "\n", - "Mek->p90RSK\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk\n", - "\n", - "Erk\n", - "\n", - "\n", - "\n", - "Mek->Erk\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_17_target\n", - "\n", - "\n", - "\n", - "\n", - "p90RSK->e_17_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "Erk->Hsp27\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_18_target\n", - "\n", - "\n", - "\n", - "\n", - "Erk->e_18_target\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_23_source\n", - "\n", - "\n", - "\n", - "\n", - "e_23_source->EGF\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "e_24_source\n", - "\n", - "\n", - "\n", - "\n", - "e_24_source->TNFa\n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "G3_prep.plot(custom_edge_attr=cno.cno_style(P_G3, flow_name=\"edge_activates\", scale=None, iexp=3))" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "cno.plot_fitness(G3_prep, exp_list_toy_full, P_G3)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/guide/signaling/cellnopt_dag.ipynb b/docs/guide/signaling/cellnopt_dag.ipynb new file mode 100644 index 000000000..a1de429e1 --- /dev/null +++ b/docs/guide/signaling/cellnopt_dag.ipynb @@ -0,0 +1,2649 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d0d6ea50", + "metadata": {}, + "source": [ + "# Inferring intracellular signaling models with CellNOptDAG\n", + "\n", + "Many signaling experiments follow the same design: stimulate a receptor or inhibit a signaling protein, wait until a chosen endpoint, and measure a panel of phosphoproteins or reporters. Each experimental condition is therefore a **snapshot of the cellular response to an intervention**.\n", + "\n", + "This method is built around one question:\n", + "\n", + "> **What is the smallest connected feed-forward signaling logic sufficient to explain the observed endpoint responses across all interventions?**\n", + "\n", + "A prior-knowledge network (PKN) supplies the candidate biology. It collects signaling interactions reported in the literature or databases and is deliberately inclusive: it may contain alternative routes, AND gates, inhibitory effects, and feedback loops. `CellNOptDAG` searches this network for a small Boolean signaling model that answers the question above across the complete experiment.\n", + "\n", + "Proteins are represented as inactive (`0`) or active (`1`), and the same pathway structure is used in every condition. The interventions change which parts of that pathway can respond. This is a simplified model of intracellular signal transmission—not a reconstruction of every molecular event between stimulation and measurement.\n", + "\n", + "The worked example develops the answer in three steps:\n", + "\n", + "1. identify which candidate signaling reactions are needed;\n", + "2. examine how the predicted pathway response changes after each stimulation or inhibition;\n", + "3. compare the predicted activities with the measured readouts.\n", + "\n", + "The main workflow explains these steps visually. The final **Under the hood** section connects the biological intuition to the Boolean and optimization formulation." + ] + }, + { + "cell_type": "markdown", + "id": "4b824f5a", + "metadata": {}, + "source": [ + "## One signaling model, several perturbation conditions\n", + "\n", + "CellNOpt learns one pathway diagram from the complete experiment. What happens on that diagram changes from one condition to another.\n", + "\n", + "| Part of the experiment | Biological meaning | Shared across conditions? |\n", + "|---|---|---|\n", + "| Selected reactions | The signaling interactions retained in the inferred pathway | Yes |\n", + "| Stimuli and inhibitors | Which proteins are experimentally forced on or off | No |\n", + "| Predicted protein activity | The model's active/inactive response after each intervention | No |\n", + "| Active reactions | The retained interactions through which signal can pass in that condition | No |\n", + "| Measured readouts | The observed endpoint response used to evaluate the prediction | No |\n", + "\n", + "A reaction may therefore belong to the inferred pathway but remain inactive in a particular condition. For example, an `A → X` reaction can be part of the shared model even when `A` was not stimulated in one experiment.\n", + "\n", + "CORNETO also checks that the retained pathway connects experimental interventions to measured responses. That computational check is explained later; it should not be interpreted as a measured biochemical flux." + ] + }, + { + "cell_type": "markdown", + "id": "5e4ca25c", + "metadata": {}, + "source": [ + "## Relationship to CellNOptR\n", + "\n", + "[CellNOptR](https://doi.org/10.1186/1752-0509-6-133) established the idea of training logic models of intracellular signaling against perturbation data. It can iteratively propagate Boolean states toward a steady state and can represent successive pseudo-steady states when several experimental times are available.\n", + "\n", + "This guide addresses the simpler case of one response snapshot per intervention. It retains the CellNOpt ideas of selecting reactions, evaluating Boolean responses in every condition, and balancing data fit against model size. CORNETO adds two restrictions for this endpoint setting: the selected model must be acyclic, and every selected reaction must belong to a connected route from an intervention to a readout. These additions are summarized in **Under the hood**." + ] + }, + { + "cell_type": "markdown", + "id": "adb8ac7f", + "metadata": {}, + "source": [ + "## Example: perturbations and two signaling readouts\n", + "\n", + "We use abstract labels so that the logic is easy to follow. `A`, `C`, and `D` play the role of experimentally controlled upstream signals; `B`, `X`, and `W` are intracellular signaling proteins; and `Y` and `Z` are measured downstream responses.\n", + "\n", + "The candidate network includes several plausible mechanisms:\n", + "\n", + "- `D → B`, so activating `D` can indirectly block `A AND NOT B → X`;\n", + "- a branch from `X` toward both readouts;\n", + "- `W AND C → Z`, representing a response that needs two signals simultaneously;\n", + "- alternative ways to activate `Y` through `X`, `C`, or the absence of `D`.\n", + "\n", + "The PKN also contains shortcuts and feedback interactions. They remain legitimate biological hypotheses. The question is whether the snapshot experiment contains enough information to retain them in the fitted endpoint model." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3ec145a6", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "import corneto as cn\n", + "from corneto.methods.signaling import (\n", + " CellNOptDAG,\n", + " plot_cellnopt_fit,\n", + " plot_cellnopt_model,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ccf4fa07", + "metadata": {}, + "source": [ + "### Define the prior-knowledge network\n", + "\n", + "Positive interactions use sign `+1`; inhibitory interactions use sign `−1`. The temporary nodes `AND1` and `AND2` are the usual SIF representation of two combinatorial signaling rules:\n", + "\n", + "- `A AND NOT B → X`: `A` must be active while `B` must be inactive;\n", + "- `W AND C → Z`: both `W` and `C` must be active.\n", + "\n", + "CORNETO treats every complete AND rule as one candidate reaction. The network also includes direct routes to `Z` and the feedback candidates `Y → Z → X` and `Z → W → Z`. Thus the PKN contains cycles even though the fitted endpoint model will not." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "29fbd626", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sourcesigntarget
0A1AND1
1B-1AND1
2AND11X
3D1B
4X1Y
5X1W
6W1AND2
7C1AND2
8AND21Z
9C1Y
10D-1Y
11W1Z
12C1Z
13Y1Z
14Z1X
15Z-1W
\n", + "
" + ], + "text/plain": [ + " source sign target\n", + "0 A 1 AND1\n", + "1 B -1 AND1\n", + "2 AND1 1 X\n", + "3 D 1 B\n", + "4 X 1 Y\n", + "5 X 1 W\n", + "6 W 1 AND2\n", + "7 C 1 AND2\n", + "8 AND2 1 Z\n", + "9 C 1 Y\n", + "10 D -1 Y\n", + "11 W 1 Z\n", + "12 C 1 Z\n", + "13 Y 1 Z\n", + "14 Z 1 X\n", + "15 Z -1 W" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkn_edges = [\n", + " (\"A\", 1, \"AND1\"),\n", + " (\"B\", -1, \"AND1\"),\n", + " (\"AND1\", 1, \"X\"),\n", + " (\"D\", 1, \"B\"),\n", + " (\"X\", 1, \"Y\"),\n", + " (\"X\", 1, \"W\"),\n", + " (\"W\", 1, \"AND2\"),\n", + " (\"C\", 1, \"AND2\"),\n", + " (\"AND2\", 1, \"Z\"),\n", + " (\"C\", 1, \"Y\"),\n", + " (\"D\", -1, \"Y\"),\n", + " (\"W\", 1, \"Z\"),\n", + " (\"C\", 1, \"Z\"),\n", + " (\"Y\", 1, \"Z\"),\n", + " (\"Z\", 1, \"X\"),\n", + " (\"Z\", -1, \"W\"),\n", + "]\n", + "pkn = cn.Graph.from_tuples(pkn_edges)\n", + "\n", + "pd.DataFrame(pkn_edges, columns=[\"source\", \"sign\", \"target\"])" + ] + }, + { + "cell_type": "markdown", + "id": "fbad6a30", + "metadata": {}, + "source": [ + "### Define the perturbation experiment\n", + "\n", + "Each row below corresponds to one experimental condition. It records what was done to the cells and what was measured afterward:\n", + "\n", + "- **input:** `1` forces an upstream signal on and `0` keeps it off, representing stimulus present or absent in this simplified experiment;\n", + "- **inhibit:** `1` applies an inhibitor and forces its target off;\n", + "- **readout:** the observed activity at the endpoint.\n", + "\n", + "`Y` and `Z` are the only measured responses. Interventions target the upstream or internal signaling proteins `A–D` and `W`, so the roles of experimental cue and downstream readout remain distinct. The interpretation column is a short label for the reader and is not used by the model.\n", + "\n", + "The informative part of the design is the contrast between rows. `C1` and `C2` receive the same upstream inputs, but only `C1` inhibits `B`; their different `Y` responses test the route `D → B` followed by the first AND gate. `C3` and `C4` test whether `C` alone is sufficient for `Z`, or whether `W` is also required. `C4` and `C6` have the same inputs, but inhibiting `W` in `C6` turns `Z` off while leaving `Y` active, directly testing the `W AND C → Z` branch." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "47eeb812", + "metadata": {}, + "outputs": [], + "source": [ + "inputs = {\n", + " \"C1\": {\"A\": 1, \"C\": 0, \"D\": 1},\n", + " \"C2\": {\"A\": 1, \"C\": 0, \"D\": 1},\n", + " \"C3\": {\"A\": 0, \"C\": 1, \"D\": 1},\n", + " \"C4\": {\"A\": 1, \"C\": 1, \"D\": 0},\n", + " \"C5\": {\"A\": 0, \"C\": 0, \"D\": 0},\n", + " \"C6\": {\"A\": 1, \"C\": 1, \"D\": 0},\n", + "}\n", + "measurements = {\n", + " \"C1\": {\"Y\": 1, \"Z\": 0},\n", + " \"C2\": {\"Y\": 0, \"Z\": 0},\n", + " \"C3\": {\"Y\": 1, \"Z\": 0},\n", + " \"C4\": {\"Y\": 1, \"Z\": 1},\n", + " \"C5\": {\"Y\": 1, \"Z\": 0},\n", + " \"C6\": {\"Y\": 1, \"Z\": 0},\n", + "}\n", + "inhibitors = {\n", + " \"C1\": {\"B\": 1},\n", + " \"C2\": {},\n", + " \"C3\": {},\n", + " \"C4\": {},\n", + " \"C5\": {},\n", + " \"C6\": {\"W\": 1},\n", + "}\n", + "behavior = {\n", + " \"C1\": \"B inhibition enables AND1\",\n", + " \"C2\": \"D activates B and blocks AND1\",\n", + " \"C3\": \"C alone\",\n", + " \"C4\": \"AND2 enabled\",\n", + " \"C5\": \"negative-literal route\",\n", + " \"C6\": \"W inhibited; Z lost\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "426761ba", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
interpretationinput Ainput Cinput Dinhibit Binhibit Wreadout Yreadout Z
condition
C1B inhibition enables AND11011010
C2D activates B and blocks AND11010000
C3C alone0110010
C4AND2 enabled1100011
C5negative-literal route0000010
C6W inhibited; Z lost1100110
\n", + "
" + ], + "text/plain": [ + " interpretation input A input C input D \\\n", + "condition \n", + "C1 B inhibition enables AND1 1 0 1 \n", + "C2 D activates B and blocks AND1 1 0 1 \n", + "C3 C alone 0 1 1 \n", + "C4 AND2 enabled 1 1 0 \n", + "C5 negative-literal route 0 0 0 \n", + "C6 W inhibited; Z lost 1 1 0 \n", + "\n", + " inhibit B inhibit W readout Y readout Z \n", + "condition \n", + "C1 1 0 1 0 \n", + "C2 0 0 0 0 \n", + "C3 0 0 1 0 \n", + "C4 0 0 1 1 \n", + "C5 0 0 1 0 \n", + "C6 0 1 1 0 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "experiment_table = pd.DataFrame(index=inputs)\n", + "experiment_table.index.name = \"condition\"\n", + "experiment_table[\"interpretation\"] = pd.Series(behavior)\n", + "for species in [\"A\", \"C\", \"D\"]:\n", + " experiment_table[f\"input {species}\"] = [inputs[name][species] for name in inputs]\n", + "for species in [\"B\", \"W\"]:\n", + " experiment_table[f\"inhibit {species}\"] = [inhibitors[name].get(species, 0) for name in inputs]\n", + "for species in [\"Y\", \"Z\"]:\n", + " experiment_table[f\"readout {species}\"] = [measurements[name][species] for name in inputs]\n", + "\n", + "experiment_table" + ] + }, + { + "cell_type": "markdown", + "id": "013eca6a", + "metadata": {}, + "source": [ + "## Fit one pathway to the complete experiment\n", + "\n", + "All six conditions are fitted together because they are assumed to probe the same underlying signaling network. The model first tries to reproduce the measured `Y` and `Z` responses. Among similarly fitting explanations, it prefers the one using fewer reactions.\n", + "\n", + "`lambda_reg` controls this preference for a smaller model. Its value is deliberately low here: agreement with the experimental readouts is more important than removing one additional reaction." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "92d6dbf8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'optimal'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "method = CellNOptDAG(lambda_reg=1e-3)\n", + "problem = method.build_many(\n", + " pkn,\n", + " inputs=inputs,\n", + " measurements=measurements,\n", + " inhibitors=inhibitors,\n", + ")\n", + "result = problem.solve(solver=\"SCIPY\")\n", + "result.status" + ] + }, + { + "cell_type": "markdown", + "id": "28a99a87", + "metadata": {}, + "source": [ + "## Which signaling reactions are supported?\n", + "\n", + "The fitted model retains `D → B`, both AND gates, and three alternative routes to `Y`. Together, these reactions explain why different interventions can produce different combinations of `Y` and `Z` activity.\n", + "\n", + "The direct shortcuts to `Z` and the feedback reactions are not needed for these observations. They remain in the PKN as biological possibilities, but this particular endpoint experiment does not support including them in the smaller fitted model." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "810e64c4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
reactionselected
0D -> B1
1X -> Y1
2X -> W1
3C -> Y1
4NOT D -> Y1
5W -> Z0
6C -> Z0
7Y -> Z0
8Z -> X0
9NOT Z -> W0
10A AND NOT B -> X1
11W AND C -> Z1
\n", + "
" + ], + "text/plain": [ + " reaction selected\n", + "0 D -> B 1\n", + "1 X -> Y 1\n", + "2 X -> W 1\n", + "3 C -> Y 1\n", + "4 NOT D -> Y 1\n", + "5 W -> Z 0\n", + "6 C -> Z 0\n", + "7 Y -> Z 0\n", + "8 Z -> X 0\n", + "9 NOT Z -> W 0\n", + "10 A AND NOT B -> X 1\n", + "11 W AND C -> Z 1" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def reaction_label(reaction):\n", + " literals = [str(node) for node in reaction.positive_literals]\n", + " literals += [f\"NOT {node}\" for node in reaction.negative_literals]\n", + " return f\"{' AND '.join(literals)} -> {reaction.product}\"\n", + "\n", + "reaction_labels = [reaction_label(reaction) for reaction in method.reactions]\n", + "selected = np.rint(problem.expr.reaction_selected.value).astype(int).reshape(-1)\n", + "pd.DataFrame({\"reaction\": reaction_labels, \"selected\": selected})" + ] + }, + { + "cell_type": "markdown", + "id": "43c08897", + "metadata": {}, + "source": [ + "### Compare the candidate network with the fitted model\n", + "\n", + "This plot shows the complete candidate network. Reactions retained in the fitted model use the main styling; alternatives that were available but not selected are pale and dotted.\n", + "\n", + "Keeping the unselected reactions visible is useful here. It shows that the feedback loops and shortcuts were offered to the model rather than silently removed from the PKN." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "95f30c93", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method, show_unselected=True)" + ] + }, + { + "cell_type": "markdown", + "id": "269f1115", + "metadata": {}, + "source": [ + "### Inspect the fitted signaling model\n", + "\n", + "Hiding unselected reactions leaves the signaling hypothesis supported by this experiment. `D → B` controls the first AND gate, `X` branches toward `Y` and `W`, and the second AND gate combines `W` with `C` to activate `Z`. Separate routes can activate `Y` under different perturbations.\n", + "\n", + "This is the shared pathway diagram. The next plots show how much of it is active in each condition." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "386f1f50", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method)" + ] + }, + { + "cell_type": "markdown", + "id": "f0aa9784", + "metadata": {}, + "source": [ + "## Does the model reproduce the measured responses?\n", + "\n", + "Each row is one intervention condition, and the `Y` and `Z` panels match the readout columns in the experiment table:\n", + "\n", + "- red open circles show the measured endpoint response;\n", + "- blue squares show the model prediction;\n", + "- a vertical segment would show a disagreement;\n", + "- the background color summarizes the prediction error.\n", + "\n", + "Only measured signaling responses are shown in this main fit view. Predicted activities of unmeasured internal proteins are inspected later, where they are clearly distinguished from experimental observations." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9438b958", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fit_figure, fit_axes = plot_cellnopt_fit(method)" + ] + }, + { + "cell_type": "markdown", + "id": "9a672bb9", + "metadata": {}, + "source": [ + "The horizontal position is labelled **Endpoint** because these data contain one post-intervention snapshot, not a time course. The plot does not create an artificial starting value or assume that the first condition is a baseline. If future formulations model several measured time points, observed and predicted values can instead be placed at their actual sampling times." + ] + }, + { + "cell_type": "markdown", + "id": "012732df", + "metadata": {}, + "source": [ + "## A pathway reaction can be present but inactive\n", + "\n", + "It helps to separate the **pathway map** from the **response in one experiment**. A road can be present on a map even when no traffic uses it; likewise, a reaction can belong to the fitted signaling model even when its upstream requirements are not satisfied in one condition.\n", + "\n", + "`C1` and `C2` have the same upstream inputs: `A=1`, `C=0`, and `D=1`. In both conditions, `D` can activate `B`. In `C1`, however, the experimental inhibitor forces `B` off. This allows the rule `A AND NOT B → X` to transmit the signal. In `C2`, `B` remains active, the `NOT B` requirement fails, and the same pathway branch is inactive and drawn with dashed lines." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9546c203", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method, condition=\"C1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c2a5b5cc", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method, condition=\"C2\")" + ] + }, + { + "cell_type": "markdown", + "id": "e1299723", + "metadata": {}, + "source": [ + "### An inhibitor changes its target, not the whole cell\n", + "\n", + "`C4` and `C6` have the same upstream inputs. Without the inhibitor in `C4`, `X` activates `W`, the rule `W AND C → Z` is satisfied, and both readouts are active.\n", + "\n", + "In `C6`, the inhibitor specifically forces internal protein `W` off. Signaling to `Y` remains active through `X` and `C`, but the AND rule producing `Z` is no longer satisfied, matching the observed `Y=1, Z=0`. The intervention therefore blocks one branch rather than being interpreted as a shutdown of the whole pathway." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8d1af7bd", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method, condition=\"C6\")" + ] + }, + { + "cell_type": "markdown", + "id": "9b4ee4e6", + "metadata": {}, + "source": [ + "## A compact view for larger signaling experiments\n", + "\n", + "When an experiment contains many perturbations or readouts, aligned heatmaps are easier to scan than one panel per response. Observed activities, predicted activities, absolute errors, and experimental cues remain aligned by condition. Unmeasured entries stay blank rather than being treated as zeros." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f12e3db5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8EAAAH3CAYAAACb9F2ZAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXddJREFUeJzt3Ql8FOX5wPFncwdCoMghyGGicitoFAWqgFbOUrW0qPUslqogeKD9F9R6IpVSq8jhSa3V2kPUakWQooIWKnJWbTiUU4EiIgQCuef/ed5k1k0IYTdks7Pz/r5+RnZmZzczu/vMzDPvFXAcxxEAAAAAACyQEOsNAAAAAACgvpAEAwAAAACsQRIMAAAAALAGSTAAAAAAwBokwQAAAAAAa5AEAwAAAACsQRIMAAAAALAGSTAAAAAAwBokwQAAAAAAa1iZBP/73/+WH//4x9KqVStJSUmR448/Xn70ox/J0qVLK6137733SiAQkN27d4ttTjzxRLn22mtjvRmIc88995yJIZ3ee++9w553HEdOPvlk83y/fv3q7O/q+2n8Rmrz5s3mtbrdsNe0adPM76Bbt241/k6mTp1ar9vlnpNq47///a95vW47ANSl//znP/LTn/5UsrKyJC0tTTIyMuSMM86QKVOmyJ49e/iw4UnWJcGPP/649OnTR7744gsTnP/85z/NhcyXX34p3/3ud2X69Omx3kTAdxo1aiTPPvvsYcsXLVokn3/+uXke8IrZs2ebfz/99FP58MMPxQ80Cb7vvvtIggHUqaefflpycnLko48+kjvuuEPmzZsnr776qilseuKJJ+S6667jE4cnJYlF/vWvf8ktt9wiQ4YMMQGalPTt7l922WVyySWXyM033yynn366SZS95NChQ5Kenh7rzQBq5dJLL5UXX3xRZsyYIZmZmcHlmhj36tVL8vLy+GThCcuXL5c1a9bI0KFD5c033zS/0bPPPjvWm+VLWhOkoKCg2nObnvO0RKm2Jd/q4MGD0qBBg2PcSgBHojUob7zxRrnwwgvltddek9TU1OBzumz8+PEmKQa8yKqS4MmTJ5sT6qxZsyolwErnZ86caZ7/9a9/Xem5bdu2yQ9/+ENz8d64cWO58sor5auvvqq0zjvvvGOqcx533HHmhN6uXTsZPny4OQm7ioqK5MEHH5ROnTqZA0Xz5s1N9ZGq76VVkb///e/LK6+8YhJyvRDQO/j6+Nxzzz1sv0pLS+WEE04w2xjp3youLpZf/OIXpkq4XixoafiyZctq+QkD1bv88svNvy+99FJw2b59+2TOnDkycuTIw9bX6lOjR482v2ttspCdnS133nmnFBYWVlpPk+dRo0aZuNPqV4MGDZL169dXuw0bNmyQn/zkJ9KiRQsTE507dzZJORDKrbGg54HevXvLn//850rH8VBlZWUyadIkc7zX4/SZZ54pCxcurLSOHnN//vOfS9u2bYPHYr3JqrWQqpY+d+/e3bxP06ZNzU3Z3NzcWlf9D23SotX7tVRG9e/fP9hEIbTav27PBRdcYM5zei7Qbay6L0eicXj77bebqpAarxq3esM5Pz//sG296aabTOmQxp9+Hn/4wx+CzSbefvttczzQz0i3QeNdP2OtteWeyzR+r776alObK5Sef7X6+uLFi833pq+v7tgCoO489NBDJnafeuqpSgmwS48HP/jBD8I+Vrl27twp119/vbRp08a8hx5b9Dq4pKSk0np6Pa/HTT3/a40yPU5MnDiRrxjhcSxRUlLiNGjQwDn77LNrXK9nz55mPV3/nnvucfQjat++vXPHHXc48+fPdx555BGnYcOGzumnn+4UFRWZ12zatMlJS0tzLrzwQue1115z3nvvPefFF190rrrqKuebb74x65SWljqDBg0yr73vvvucBQsWOM8884xzwgknOF26dHEOHjwY3Ab9e61atXKys7Od2bNnO++++66zbNky57HHHjPbs379+krbPHfuXLP89ddfj/hvXXPNNU4gEDD79/bbb5v90/UyMzPNc8Cx+P3vf29+mx999JGJB40v16xZs8xvNC8vz+natavTt29fs/zQoUPOaaedZp6bOnWq+V3efffdTlJSkjNkyJDg68vKypz+/fs7qampzqRJk8x6GrMaN/o39bHr008/dRo3buyceuqpzvPPP2/WHT9+vJOQkODce++9wfU0lvW1ut2wjx4b9Xdy1llnmXk9burv4bnnnqu0nvs7adu2rfPd737XmTNnjvO3v/3NvC45OdlZsmRJcN2BAwc6zZs3d5566ilzbtBzxK9+9Svnz3/+c3Cdhx56yLzf5Zdf7rz55pvmN6q/Y92W0OO9e04KVfW3HnoecY/hu3btCv6NGTNmOEuXLjWTLld//OMfzXng4osvdl555RXn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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "heatmap_figure, heatmap_axes = plot_cellnopt_fit(method, view=\"heatmap\")" + ] + }, + { + "cell_type": "markdown", + "id": "f4d57529", + "metadata": {}, + "source": [ + "## Under the hood\n", + "\n", + "The biological workflow above rests on a few modeling assumptions. This section states only the assumptions needed to interpret the result and separates the CellNOpt core from the acyclicity and connectivity requirements added by CORNETO." + ] + }, + { + "cell_type": "markdown", + "id": "499bc91f", + "metadata": {}, + "source": [ + "### Endpoint Boolean assumptions\n", + "\n", + "One reaction set is selected for the whole experiment, while protein and reaction activities are predicted separately for each condition.\n", + "\n", + "For every condition:\n", + "\n", + "- a selected reaction is active only when all its positive inputs are active and all its negative inputs are inactive;\n", + "- several active reactions producing the same protein act as OR alternatives;\n", + "- a stimulus or inhibitor overrides the usual production rule by fixing its target on or off;\n", + "- internal activities are predictions at the measured endpoint, not simulated intermediate time steps.\n", + "\n", + "CORNETO represents these rules with vectorized binary variables: $Y_r$ for shared reaction selection, $Z_{rc}$ for condition-specific reaction activity, and $X_{vc}$ for condition-specific protein activity. The implementation uses exact linear constraints for AND, OR, and negative literals, but those linearization details do not change the biological assumptions above." + ] + }, + { + "cell_type": "markdown", + "id": "6b6b6971", + "metadata": {}, + "source": [ + "### Fit and model size\n", + "\n", + "The model prioritizes agreement with observed readouts and uses a smaller penalty to prefer compact signaling explanations:\n", + "\n", + "$$\n", + "\\min\\; \\sum_{(v,c)\\,\\mathrm{observed}} |X_{vc}-m_{vc}| + \\lambda\\sum_r Y_r.\n", + "$$\n", + "\n", + "Only measured entries contribute to the first term. Unmeasured proteins are not treated as zero. The second term counts selected Boolean reactions, so an AND gate is penalized once rather than once per input leg." + ] + }, + { + "cell_type": "markdown", + "id": "180b830e", + "metadata": {}, + "source": [ + "### What CORNETO adds\n", + "\n", + "| Modeling element | Role in this formulation |\n", + "|---|---|\n", + "| Shared reaction selection across conditions | CellNOpt core |\n", + "| Condition-specific Boolean AND, OR, and inhibitory logic | CellNOpt core |\n", + "| Readout mismatch plus a model-size penalty | CellNOpt core |\n", + "| Acyclic selected reaction dependencies | CORNETO endpoint restriction |\n", + "| One shared structural flow connecting interventions to readouts | CORNETO connectivity extension |\n", + "\n", + "Acyclicity is imposed on the **selected model**, not on the PKN. CORNETO assigns an ordering level $h_v$ to every protein and requires each selected dependency $u\\rightarrow v$ to point forward:\n", + "\n", + "$$\n", + "Y_r=1 \\;\\Longrightarrow\\; h_u < h_v\n", + "\\qquad\\text{for every dependency }u\\rightarrow v\\text{ in reaction }r.\n", + "$$\n", + "\n", + "This makes the endpoint response evaluable from the experimental inputs without choosing an update order or allowing a selected feedback loop to support itself.\n", + "\n", + "Connectivity is a separate requirement. CORNETO sends an artificial bookkeeping flow from controlled proteins, through every selected dependency, to measured readouts. Selected dependencies must carry positive flow, unselected dependencies carry none, and flow is conserved at internal proteins. The resulting flow is a structural certificate that the selected model contains no disconnected or dangling reactions." + ] + }, + { + "cell_type": "markdown", + "id": "e7f13c08", + "metadata": {}, + "source": [ + "### Inspecting the inferred states\n", + "\n", + "The tables below contain only selected reactions and the internal or measured proteins needed to interpret the example. Reaction activity and protein activity vary by condition; neither table contains additional experimental measurements. `Y` and `Z` are observed readouts, whereas `B`, `X`, and `W` are model predictions." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "39c46bc9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
C1C2C3C4C5C6
D -> B111000
X -> Y100101
X -> W100101
C -> Y001101
NOT D -> Y000111
A AND NOT B -> X100101
W AND C -> Z000100
\n", + "
" + ], + "text/plain": [ + " C1 C2 C3 C4 C5 C6\n", + "D -> B 1 1 1 0 0 0\n", + "X -> Y 1 0 0 1 0 1\n", + "X -> W 1 0 0 1 0 1\n", + "C -> Y 0 0 1 1 0 1\n", + "NOT D -> Y 0 0 0 1 1 1\n", + "A AND NOT B -> X 1 0 0 1 0 1\n", + "W AND C -> Z 0 0 0 1 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
C1C2C3C4C5C6
B011000
X100101
W100100
Y101111
Z000100
\n", + "
" + ], + "text/plain": [ + " C1 C2 C3 C4 C5 C6\n", + "B 0 1 1 0 0 0\n", + "X 1 0 0 1 0 1\n", + "W 1 0 0 1 0 0\n", + "Y 1 0 1 1 1 1\n", + "Z 0 0 0 1 0 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "condition_names = list(inputs)\n", + "selected_mask = selected.astype(bool)\n", + "reaction_activity = pd.DataFrame(\n", + " np.rint(problem.expr.reaction_active.value).astype(int),\n", + " index=reaction_labels,\n", + " columns=condition_names,\n", + ").loc[selected_mask]\n", + "species_states = pd.DataFrame(\n", + " np.rint(problem.expr.vertex_value.value).astype(int),\n", + " index=method.processed_graph.V,\n", + " columns=condition_names,\n", + ").loc[[\"B\", \"X\", \"W\", \"Y\", \"Z\"]]\n", + "\n", + "display(reaction_activity, species_states)" + ] + }, + { + "cell_type": "markdown", + "id": "362e287c", + "metadata": {}, + "source": [ + "### Structural connectivity diagnostic\n", + "\n", + "The following plot visualizes the artificial structural flow by edge width. It is useful for checking that selected reactions are connected to experimental inputs and measured outputs, and for finding an incorrectly mapped, disconnected, or dangling reaction.\n", + "\n", + "This is a debugging view. Flow is shared across all conditions and records neither whether a reaction is active in one condition nor how strongly a biochemical signal passes through it." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "fb593712", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "D\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->B\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "D->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "B->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "X->W\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11\n", + "\n", + "\n", + "AND\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "W->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "C\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->Y\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "C->__cellnopt_and_11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "A\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "A->__cellnopt_and_10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_10->X\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "__cellnopt_and_11->Z\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_cellnopt_model(method, width_by=\"flow\")" + ] + }, + { + "cell_type": "markdown", + "id": "96461460", + "metadata": {}, + "source": [ + "All selected dependency legs happen to carry structural flow `1` in this solution, so they receive the same width. That equality does **not** mean the reactions have equal biological strength or importance. It only means that this particular bookkeeping flow can certify every selected dependency with the minimum required amount.\n", + "\n", + "Structural-flow values are generally non-unique and solver-dependent; a different feasible certificate can assign different widths without changing the selected signaling model. The values are unrelated to protein activity, signal strength, reaction rate, confidence, or causal importance. Use the plot only to verify selected-model connectivity." + ] + }, + { + "cell_type": "markdown", + "id": "7c0e516e", + "metadata": {}, + "source": [ + "## Biological interpretation and limitations\n", + "\n", + "This method is designed for intracellular signaling experiments in which cells are stimulated or inhibited and measured once at a chosen endpoint. The result is one compact pathway structure whose predicted activity changes across intervention conditions.\n", + "\n", + "The fitted model answers the question posed at the beginning: it is a connected feed-forward signaling logic sufficient to explain these observed snapshots. “Sufficient” does not mean unique, dynamically complete, or necessarily identical to the pathway operating in the cell.\n", + "\n", + "Feedback remains biologically plausible. A snapshot with both `Y` and `Z` active usually cannot reveal whether `Y` activated `Z`, `Z` activated `Y`, an upstream protein activated both, or a loop maintained them. The acyclic restriction therefore reflects what this experiment can identify; it is not evidence that the biological network contains no feedback. Feedback candidates remain visible in the PKN as hypotheses that were available but not established by this endpoint analysis.\n", + "\n", + "A dynamic model is more appropriate when measurements cover activation and relaxation over time, proteins inside a proposed loop are directly perturbed, or the data show adaptation, oscillation, or dependence on initial state. Such analyses must state how time and updates are represented.\n", + "\n", + "Other limitations remain: Boolean states simplify graded and stochastic signaling; inference is restricted to the supplied PKN; alternative models may fit equally well; and unmeasured internal activities remain predictions. Structural flow certifies connectivity only—it does not quantify signaling strength." + ] + }, + { + "cell_type": "markdown", + "id": "causal-structure-bridge", + "metadata": {}, + "source": [ + "## Connection to causal structure learning\n", + "\n", + "For readers coming from mathematics or causal inference, `CellNOptDAG` can be viewed as **interventional Boolean causal structure learning over a restricted hypothesis space**.\n", + "\n", + "The variables are the binary protein activities $X_v$. The PKN acts as a causal *superstructure*: it specifies the allowed variables, directions, signs, and multi-input clauses. The optimization does not invent interactions outside that superstructure. Instead, it chooses which candidate reactions are needed to explain all intervention conditions.\n", + "\n", + "Let $Y_r$ indicate whether candidate reaction $r$ is selected. If $P_r$ and $N_r$ are its positive and negative inputs, its condition-specific truth value is\n", + "\n", + "$$\n", + "Z_{rc}=Y_r\\land\\bigwedge_{u\\in P_r}X_{uc}\\land\\bigwedge_{w\\in N_r}\\neg X_{wc}.\n", + "$$\n", + "\n", + "For a protein that is not experimentally fixed, alternative selected reactions producing that protein act as OR alternatives:\n", + "\n", + "$$\n", + "X_{vc}=\\bigvee_{r:\\,\\operatorname{product}(r)=v} Z_{rc}.\n", + "$$\n", + "\n", + "The inferred equation for each protein is therefore a signed Boolean expression in disjunctive normal form. For example, selecting `A AND NOT B → X` and `C → X` gives\n", + "\n", + "$$\n", + "X=(A\\land\\neg B)\\lor C.\n", + "$$\n", + "\n", + "The selection variables $Y_r$ are shared across conditions: this is the assumption that the underlying causal mechanisms remain invariant throughout the experiment. The state variables $X_{vc}$ and reaction activities $Z_{rc}$ vary because each condition applies different experimental manipulations.\n", + "\n", + "A stimulus or inhibitor overrides the usual equation for its target and fixes that variable to `1` or `0`. In structural-causal notation, this plays the role of a hard intervention such as $\\operatorname{do}(X_v=1)$ or $\\operatorname{do}(X_v=0)$. CellNOptDAG then searches for a shared model that predicts the measured outcomes of these interventions:\n", + "\n", + "$$\n", + "\\min_Y\\;\\sum_{(v,c)\\,\\mathrm{observed}}|X_{vc}-m_{vc}|+\\lambda\\sum_rY_r.\n", + "$$\n", + "\n", + "The first term is interventional prediction error and the second is a sparsity penalty on structural mechanisms. Because CORNETO also requires the selected dependencies to be acyclic, the fitted Boolean structural equations can be evaluated in topological order without specifying feedback dynamics.\n", + "\n", + "| CellNOpt term | Causal or mathematical interpretation |\n", + "|---|---|\n", + "| Protein activity | Binary endogenous variable |\n", + "| PKN | Causal superstructure or hypothesis space |\n", + "| Selected reaction | Selected structural mechanism or Boolean clause |\n", + "| Stimulus or inhibitor | Hard intervention |\n", + "| Shared reaction selection | Invariant causal structure |\n", + "| Readout mismatch | Interventional prediction loss |\n", + "| Model-size penalty | Sparsity regularization |\n", + "| Acyclicity | Topological evaluation of the structural equations; feedback is excluded |\n", + "| Structural flow | Connectivity certificate, not a causal-effect estimate |\n", + "\n", + "The phrase **causal discovery** needs one qualification here. This is not unrestricted discovery of a causal graph from data alone: the PKN already supplies the possible mechanisms, including the available AND clauses. The method selects among those mechanisms, while OR relationships arise from selecting several reactions with the same product. It is therefore more precise to call the task **PKN-constrained interventional Boolean causal structure learning**.\n", + "\n", + "As in other causal-learning problems, the fitted structure need not be identifiable. Two candidate subnetworks may make exactly the same predictions under the available interventions. The size penalty can prefer a compact representative, but it cannot turn experimentally indistinguishable models into a uniquely established causal explanation." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/guide/signaling/index.md b/docs/guide/signaling/index.md index 8b67a5cc9..9855bd1e8 100644 --- a/docs/guide/signaling/index.md +++ b/docs/guide/signaling/index.md @@ -9,5 +9,5 @@ carnival.ipynb multisample-carnival.ipynb phonemes.ipynb bidirectional-phonemes.ipynb -cellnopt_ILP.ipynb +cellnopt_dag.ipynb ``` diff --git a/docs/install.md b/docs/install.md index defa01b62..deed8eccc 100644 --- a/docs/install.md +++ b/docs/install.md @@ -38,6 +38,7 @@ pip install corneto CORNETO provides several optional dependency groups: +- **`plot`**: Graphviz, NetworkX, and Matplotlib visualization support - **`research`**: Full research stack with Gurobi, PICOS, visualization, and network tools - **`os`**: Open-source solvers (SCIP, HiGHS) with visualization and network tools - **`ml`**: Machine learning dependencies (JAX, Keras, scikit-learn) @@ -45,6 +46,7 @@ CORNETO provides several optional dependency groups: Install any combination with: ```bash +pip install corneto[plot] # Visualization without the full research stack pip install corneto[research,ml] # Multiple extras ``` diff --git a/docs/releases/index.md b/docs/releases/index.md index c573b582d..49e315d35 100644 --- a/docs/releases/index.md +++ b/docs/releases/index.md @@ -7,6 +7,7 @@ This section contains detailed release notes for CORNETO versions, documenting n ```{toctree} :maxdepth: 1 +v1.0.0-rc.3 v1.0.0-rc.1 migration-1.0 v1.0.0-beta.2 @@ -41,7 +42,7 @@ CORNETO follows [semantic versioning](https://semver.org/) with the following re - **GitHub Releases**: Follow releases on the [GitHub repository](https://github.com/saezlab/corneto/releases) - **PyPI**: Install the latest version with `pip install --upgrade corneto` -- **Development**: Track development progress on the `dev` branch +- **Development**: Track development through pull requests into `main` ## Contributing to Releases diff --git a/docs/releases/migration-1.0.md b/docs/releases/migration-1.0.md index 8e6f219b4..5cad33060 100644 --- a/docs/releases/migration-1.0.md +++ b/docs/releases/migration-1.0.md @@ -14,13 +14,12 @@ were removed instead of being carried as permanent compatibility aliases. | `corneto.methods.future.MultiSampleIMAT` | `corneto.methods.MultiSampleIMAT` | | `corneto.methods.future.PrizeCollectingSteinerTree` | `corneto.methods.PrizeCollectingSteinerTree` | | `corneto.methods.future.SteinerTreeFlow` | `corneto.methods.SteinerTreeFlow` | -| `corneto.methods.signal.cellnopt_ilp` | `corneto.methods.signaling.cellnopt_ilp` | | `corneto.K` or `corneto.ops` | `corneto.opt` | | `corneto._ml.build_dagnn` | `corneto.ml.build_dagnn` | | `corneto.methods.shortest_path.create_multisample_shortest_path` | `corneto.methods.create_multisample_shortest_path` | | module-level graph serialization | `Graph.save` and `Graph.load` | -The old `future`, `signal`, `K`, and `ops` paths in this table remain warning +The old `future`, `K`, and `ops` paths in this table remain warning compatibility paths throughout 1.x. The private `_ml` path and duplicate graph serialization functions do not have shims. @@ -34,8 +33,9 @@ serialization functions do not have shims. modules (`corneto._graph`, `corneto._data`, `corneto._io`, `corneto._nx`, `corneto._core`, and `corneto._legacy`). - The British-spelling `corneto.methods.signalling` implementation and the old - module-shaped `corneto.methods.signaling`; signaling methods now live in the - `corneto.methods.signaling` package. + module-shaped `corneto.methods.signaling`, as well as the singular + `corneto.methods.signal` compatibility namespace; signaling methods now live + in the `corneto.methods.signaling` package. - Duplicate module-level graph serialization helpers from `corneto.io`. ## Multi-condition convention diff --git a/docs/releases/v1.0.0-rc.1.md b/docs/releases/v1.0.0-rc.1.md index 758a8336a..90e83984c 100644 --- a/docs/releases/v1.0.0-rc.1.md +++ b/docs/releases/v1.0.0-rc.1.md @@ -20,9 +20,11 @@ It requires Python 3.11 or newer and is tested on Python 3.11, 3.12, and 3.13. ## Compatibility window -Imports through `corneto.methods.future`, `corneto.methods.signal`, and the root -`corneto.K` and `corneto.ops` aliases continue to work with `FutureWarning` -through the CORNETO 1.x line. They are scheduled for removal in CORNETO 2.0. +Imports through `corneto.methods.future` and the root `corneto.K` and +`corneto.ops` aliases continue to work with `FutureWarning` through the CORNETO +1.x line. They are scheduled for removal in CORNETO 2.0. The experimental +`corneto.methods.signal` namespace and `cellnoptILP` function are outside this +compatibility window and are removed before the final 1.0 release. See the [1.0 migration guide](migration-1.0.md) for canonical mappings and intentional removals. diff --git a/docs/releases/v1.0.0-rc.3.md b/docs/releases/v1.0.0-rc.3.md new file mode 100644 index 000000000..fbd7b7bc6 --- /dev/null +++ b/docs/releases/v1.0.0-rc.3.md @@ -0,0 +1,27 @@ +# CORNETO 1.0.0 RC3 + +CORNETO 1.0.0 RC3 expands the candidate 1.0 signaling and metabolism workflows +and moves publication to a public-`main` release gate. + +## Highlights + +- `CellNOptDAG` provides class-based Boolean signaling-model inference, + including dedicated model and fit visualizations. +- The FBA and iMAT guides have been overhauled, including a new standalone iMAT + guide and refreshed multi-condition examples. +- Public releases now use one pull request directly into public `main`; that PR + runs the Python 3.11–3.13 tests, quality checks, warning-free documentation + build, and package smoke test before a release tag is created. + +## Experimental API cleanup + +The experimental `cellnoptILP` function and `corneto.methods.signal` namespace +have been removed without compatibility shims. Use the supported class-based +API instead: + +```python +from corneto.methods.signaling import CellNOptDAG +``` + +See the [1.0 migration guide](migration-1.0.md) for the complete supported +surface and intentional removals. diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index d6444d0cb..e878c13f9 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -72,9 +72,9 @@ git clone https://github.com/saezlab/corneto cd corneto ``` -### 2. Switch to the `dev` Branch +### 2. Start from the `main` Branch ```bash -git checkout dev +git checkout main ``` ### 3. Create a New Branch @@ -121,7 +121,7 @@ git push origin contrib/my-tutorial-name ### 8. Open a Pull Request -Open a **PR against `dev`** with title: +Open a **PR against `main`** with title: ``` docs(tutorial): add my_tutorial_name example ``` diff --git a/pyproject.toml b/pyproject.toml index 8202eb7de..441233dec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,11 @@ repository = "https://github.com/saezlab/corneto/" release = "corneto.release:main" [project.optional-dependencies] +plot = [ + "networkx>=3.2.1", + "matplotlib>=3.5.2", + "graphviz>=0.20.1", +] annnet = [ "annnet[polars]>=0.2.0,<0.3" ] diff --git a/scripts/generate_switcher.py b/scripts/generate_switcher.py index 38efa5ad8..468a7edd6 100644 --- a/scripts/generate_switcher.py +++ b/scripts/generate_switcher.py @@ -37,12 +37,7 @@ def build_entries(base_url: str) -> list[dict[str, object]]: "version": "stable", "url": f"{base_url}/stable/", "preferred": True, - }, - { - "name": "latest", - "version": "latest", - "url": f"{base_url}/latest/", - }, + } ] for tag in _get_tags(): entries.append( diff --git a/tests/graph/test_plotting.py b/tests/graph/test_plotting.py index 764d66ece..a663b91f9 100644 --- a/tests/graph/test_plotting.py +++ b/tests/graph/test_plotting.py @@ -1,9 +1,10 @@ import sys from types import ModuleType +import numpy as np import pytest -from corneto._plotting import to_dot_source +from corneto._plotting import _scaled_magnitudes, _select_solution_sample, to_dot_source from corneto._util import supports_html from corneto.contrib._util import ( DEFAULT_WASM_GRAPHVIZ_JS_URL, @@ -55,6 +56,64 @@ def test_to_dot_source_honors_global_biological_node_shape(): assert '"e_0_source" [shape="point"]' in src +def test_to_dot_source_includes_isolated_vertices(): + g = Graph() + g.add_vertex("isolated") + + src = to_dot_source( + g, + custom_vertex_attr={"isolated": {"shape": "box", "label": "Cue"}}, + ) + + assert '"isolated" [shape="box", label="Cue"]' in src + + +def test_scaled_magnitudes_preserve_zero_and_handle_constant_values(): + assert np.allclose(_scaled_magnitudes(np.zeros(3)), np.zeros(3)) + assert np.allclose( + _scaled_magnitudes(np.array([2.0, 2.0, 0.0]), scale="log", clip_quantile=0.05), + [1.0, 1.0, 0.0], + ) + scaled = _scaled_magnitudes( + np.array([0.0, 1.0, 10.0, 1e6]), + scale="log", + clip_quantile=0.25, + ) + assert np.all(np.isfinite(scaled)) + assert scaled[0] == 0 + assert np.all(np.diff(scaled) >= 0) + assert scaled[-1] == 1 + + +def test_solution_sample_selection_supports_named_samples(): + data = Data.from_cdict( + { + "first": {}, + "second": {}, + } + ) + selected = _select_solution_sample( + { + "vertex_values": np.array([[1.0, -1.0], [0.0, 2.0]]), + "edge_values": np.array([[3.0, 4.0]]), + }, + sample="second", + feature_data=data, + ) + + assert np.allclose(selected["vertex_values"], [-1.0, 2.0]) + assert np.allclose(selected["edge_values"], [4.0]) + + +def test_solution_sample_selection_requires_sample_for_matrix(): + with pytest.raises(ValueError, match="select one with sample"): + _select_solution_sample( + {"vertex_values": np.ones((2, 2))}, + sample=None, + feature_data=None, + ) + + def test_dot_wasm_html_accepts_raw_dot_source(): html = dot_wasm_html('digraph {"A" -> "B";}') assert DEFAULT_WASM_GRAPHVIZ_JS_URL in html @@ -178,6 +237,31 @@ def _fake_plot_with_networkx(graph, **kwargs): assert obj is sentinel_fig +def test_networkx_renderer_uses_filtered_shared_plot_model(): + pytest.importorskip("matplotlib") + pytest.importorskip("networkx") + g = Graph() + g.add_edge("A", "B") + g.add_edge("B", "C") + + figure = g.plot( + renderer="networkx", + edge_indexes=[1], + custom_vertex_attr={ + "B": {"shape": "box", "fillcolor": "#9ACD32", "style": "filled"}, + "C": {"shape": "diamond", "label": "Output"}, + }, + custom_edge_attr={1: {"color": "#C43C39", "penwidth": "3", "arrowhead": "tee"}}, + ) + + axis = figure.axes[0] + labels = {text.get_text() for text in axis.texts} + assert "A" not in labels + assert "B" in labels + assert "Output" in labels + assert len(axis.patches) == 1 + + def test_plot_values_uses_plot_renderer_path(monkeypatch): g = Graph() g.add_edge("A", "B") diff --git a/tests/methods/signaling/test_cellnopt_dag.py b/tests/methods/signaling/test_cellnopt_dag.py new file mode 100644 index 000000000..15e2bb6f6 --- /dev/null +++ b/tests/methods/signaling/test_cellnopt_dag.py @@ -0,0 +1,266 @@ +import numpy as np + +from corneto.backend import PicosBackend +from corneto.graph import Graph +from corneto.methods.signaling.cellnopt_dag import CellNOptDAG + + +def _vertex_values(method, problem, vertex): + index = method.processed_graph.V.index(vertex) + values = np.asarray(problem.expr.vertex_value.value, dtype=float) + return values.reshape(problem.expr.vertex_value.shape)[index].reshape(-1) + + +def _flat_values(expression): + return np.asarray(expression.value, dtype=float).reshape(expression.shape).reshape(-1) + + +def _assert_infeasible(problem, backend): + if isinstance(backend, PicosBackend): + result = problem.solve(primals=None) + else: + result = problem.solve() + assert result.status == "infeasible" + + +def _solve(method, graph, *, inputs, measurements, inhibitors=None): + problem = method.build_many( + graph, + inputs=inputs, + measurements=measurements, + inhibitors=inhibitors, + ) + result = problem.solve() + assert result.status == "optimal" + return problem + + +def test_and_gate_is_one_reaction_with_conjunctive_truth(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "AND1"), + ("B", 1, "AND1"), + ("AND1", 1, "Y"), + ] + ) + inputs = { + "neither": {"A": 0, "B": 0}, + "a_only": {"A": 1, "B": 0}, + "b_only": {"A": 0, "B": 1}, + "both": {"A": 1, "B": 1}, + } + measurements = { + "neither": {"Y": 0}, + "a_only": {"Y": 0}, + "b_only": {"Y": 0}, + "both": {"Y": 1}, + } + + method = CellNOptDAG(lambda_reg=1e-3, backend=backend) + problem = _solve( + method, + graph, + inputs=inputs, + measurements=measurements, + ) + + assert len(method.reactions) == 1 + assert method.reactions[0].positive_literals == ("A", "B") + assert np.allclose(_flat_values(problem.expr.reaction_selected), [1]) + assert np.allclose( + np.asarray(problem.expr.reaction_active.value).reshape(1, -1), + [[0, 0, 0, 1]], + ) + assert np.allclose(_vertex_values(method, problem, "Y"), [0, 0, 0, 1]) + assert np.all(np.asarray(problem.expr.flow.value)[:2] >= method.epsilon) + + +def test_or_is_induced_by_multiple_active_producing_reactions(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "Y"), + ("B", 1, "Y"), + ] + ) + inputs = { + "neither": {"A": 0, "B": 0}, + "a_only": {"A": 1, "B": 0}, + "b_only": {"A": 0, "B": 1}, + "both": {"A": 1, "B": 1}, + } + measurements = { + "neither": {"Y": 0}, + "a_only": {"Y": 1}, + "b_only": {"Y": 1}, + "both": {"Y": 1}, + } + + method = CellNOptDAG(lambda_reg=1e-3, backend=backend) + problem = _solve( + method, + graph, + inputs=inputs, + measurements=measurements, + ) + + assert np.allclose(_flat_values(problem.expr.reaction_selected), [1, 1]) + assert np.allclose( + np.asarray(problem.expr.reaction_active.value), + [[0, 1, 0, 1], [0, 0, 1, 1]], + ) + assert np.allclose(_vertex_values(method, problem, "Y"), [0, 1, 1, 1]) + + +def test_positive_and_negative_alternatives_fit_complementary_conditions(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "Y"), + ("A", -1, "Y"), + ] + ) + method = CellNOptDAG(lambda_reg=1e-3, backend=backend) + problem = _solve( + method, + graph, + inputs={"off": {"A": 0}, "on": {"A": 1}}, + measurements={"off": {"Y": 1}, "on": {"Y": 1}}, + ) + + assert np.allclose(_flat_values(problem.expr.reaction_selected), [1, 1]) + assert np.allclose( + np.asarray(problem.expr.reaction_active.value), + [[0, 1], [1, 0]], + ) + assert np.allclose(_vertex_values(method, problem, "Y"), [1, 1]) + + +def test_same_inputs_with_conflicting_measurements_cannot_both_be_fit(backend): + graph = Graph.from_tuples([("A", 1, "Y")]) + method = CellNOptDAG(lambda_reg=0, backend=backend) + problem = _solve( + method, + graph, + inputs={"first": {"A": 1}, "second": {"A": 1}}, + measurements={"first": {"Y": 0}, "second": {"Y": 1}}, + ) + + assert np.isclose(problem.objectives[0].value, 1) + predictions = _vertex_values(method, problem, "Y") + assert np.allclose(predictions, [0, 0]) or np.allclose(predictions, [1, 1]) + + +def test_irrelevant_active_input_does_not_force_a_disconnected_branch(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "B"), + ("C", 1, "D"), + ] + ) + method = CellNOptDAG(lambda_reg=1e-3, backend=backend) + problem = _solve( + method, + graph, + inputs={"condition": {"A": 1, "C": 1}}, + measurements={"condition": {"B": 1}}, + ) + + assert np.allclose(_flat_values(problem.expr.reaction_selected), [1, 0]) + assert np.allclose(_vertex_values(method, problem, "C"), [1]) + assert np.allclose(_vertex_values(method, problem, "D"), [0]) + + +def test_flow_rejects_a_selected_component_without_experimental_boundaries(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "B"), + ("C", 1, "D"), + ] + ) + method = CellNOptDAG(lambda_reg=0, backend=backend) + problem = method.build( + graph, + inputs={"A": 1}, + measurements={"B": 1}, + ) + problem += problem.expr.reaction_selected[1] == 1 + + _assert_infeasible(problem, backend) + + +def test_flow_rejects_a_selected_dangling_branch(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "B"), + ("A", 1, "C"), + ] + ) + method = CellNOptDAG(lambda_reg=0, backend=backend) + problem = method.build( + graph, + inputs={"A": 1}, + measurements={"B": 1}, + ) + problem += problem.expr.reaction_selected[1] == 1 + + _assert_infeasible(problem, backend) + + +def test_inhibition_overrides_product_without_disabling_reaction_truth(backend): + graph = Graph.from_tuples([("A", 1, "B")]) + method = CellNOptDAG(lambda_reg=0, backend=backend) + problem = method.build( + graph, + inputs={"A": 1}, + inhibitors={"B": 1}, + measurements={"B": 0}, + ) + problem += problem.expr.reaction_selected[0] == 1 + + result = problem.solve() + + assert result.status == "optimal" + assert np.allclose(problem.expr.reaction_active.value, [[1]]) + assert np.allclose(_vertex_values(method, problem, "B"), [0]) + + +def test_acyclicity_rejects_a_selected_feedback_cycle(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "B"), + ("B", 1, "A"), + ] + ) + method = CellNOptDAG(lambda_reg=0, backend=backend) + problem = method.build( + graph, + inputs={"A": 1}, + measurements={"B": 1}, + ) + problem += problem.expr.reaction_selected == 1 + + _assert_infeasible(problem, backend) + + +def test_constraint_blocks_are_vectorized_over_reactions_and_conditions(backend): + small_graph = Graph.from_tuples([("A", 1, "B")]) + large_graph = Graph.from_tuples([(f"v{i}", 1, f"v{i + 1}") for i in range(30)]) + small = CellNOptDAG(lambda_reg=0, backend=backend).build( + small_graph, + inputs={"A": 1}, + measurements={"B": 1}, + ) + large = CellNOptDAG(lambda_reg=0, backend=backend).build_many( + large_graph, + inputs={ + "one": {"v0": 1}, + "two": {"v0": 1}, + "three": {"v0": 1}, + }, + measurements={ + "one": {"v30": 1}, + "two": {"v30": 1}, + "three": {"v30": 1}, + }, + ) + + assert len(small.constraints) == len(large.constraints) diff --git a/tests/methods/signaling/test_cellnopt_ilp.py b/tests/methods/signaling/test_cellnopt_ilp.py deleted file mode 100644 index 32e11ffaa..000000000 --- a/tests/methods/signaling/test_cellnopt_ilp.py +++ /dev/null @@ -1,116 +0,0 @@ -import numpy as np -import pytest - -import corneto as cn -from corneto.backend import Backend, CvxpyBackend, PicosBackend -from corneto.methods.signaling.cellnopt_ilp import cellnoptILP - - -# PICOS does not work -@pytest.fixture(params=[CvxpyBackend, PicosBackend]) -def backend(request): - opt: Backend = request.param() - if isinstance(opt, CvxpyBackend): - opt._default_solver = "SCIPY" - elif isinstance(opt, PicosBackend): - opt._default_solver = "glpk" - return opt - - -def get_test_graph_1(): - G1 = cn.Graph.from_tuples( - [ - ("EGF", 1, "AND1"), - ("TNFa", 1, "AND1"), - ("AND1", 1, "Ras"), - ("EGF", 1, "Ras"), - ("TNFa", 1, "Ras"), - ] - ) - G1.add_edge((), "EGF") - G1.add_edge((), "TNFa") - G1.add_edge("Ras", ()) - - return G1 - - -@pytest.mark.skip(reason="Error with PICOS solvers") -def test_cellnoptILP_AND(backend): - G1 = get_test_graph_1() - - # RAS is only active iff both EGF and TNFa are active -> we need to identify the AND gate - exp_list_G1_and = { - "exp0": {"input": {"EGF": 0, "TNFa": 0}, "output": {"Ras": 0}}, - "exp1": {"input": {"EGF": 1, "TNFa": 0}, "output": {"Ras": 0}}, - "exp2": {"input": {"EGF": 0, "TNFa": 1}, "output": {"Ras": 0}}, - "exp3": {"input": {"EGF": 1, "TNFa": 1}, "output": {"Ras": 1}}, - } - - P = cellnoptILP(G1, exp_list_G1_and, verbose=True, alpha_flow=0.001, backend=backend) - expected_edge_values = np.array( - [ - [0.0, 1.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0], - [-0.0, -0.0, -0.0, 1.0], - [0.0, -0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, -0.0], - [0.0, 1.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 1.0], - ] - ) - - # vertices do not have a specific order (set of vertices is a list, but the order is not fixed) - expected_vertex_values = np.array( - [ - [-0.0, -0.0, -0.0, 1.0], - [-0.0, 1.0, -0.0, 1.0], - [-0.0, -0.0, 1.0, 1.0], - [0.0, -0.0, -0.0, 1.0], - ] - ) - sum([o.value for o in P.objectives]) - assert np.isclose(sum([o.value for o in P.objectives]), 0.006) - assert np.isclose(P.expr.edge_activates.value, expected_edge_values).all() - assert np.isclose(np.sum(P.expr.vertex_value.value, axis=0), expected_vertex_values.sum(axis=0)).all() - - -# @pytest.mark.skip(reason="not compatible with picos") -def test_cellnoptILP_OR(backend): - G1 = get_test_graph_1() - - # RAS is only active iff both EGF and TNFa are active -> we need to identify the AND gate - exp_list_G1_or = { - "exp0": {"input": {"EGF": 0, "TNFa": 0}, "output": {"Ras": 0}}, - "exp1": {"input": {"EGF": 1, "TNFa": 0}, "output": {"Ras": 1}}, - "exp2": {"input": {"EGF": 0, "TNFa": 1}, "output": {"Ras": 1}}, - "exp3": {"input": {"EGF": 1, "TNFa": 1}, "output": {"Ras": 1}}, - } - - P = cellnoptILP(G1, exp_list_G1_or, verbose=True, alpha_flow=0.001, backend=backend) - expected_edge_values = np.array( - [ - [0.0, -0.0, 0.0, -0.0], - [0.0, 0.0, -0.0, -0.0], - [-0.0, -0.0, -0.0, -0.0], - [0.0, 1.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0], - [0.0, 1.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0], - [0.0, 1.0, 1.0, 1.0], - ] - ) - - # vertices do not have a specific order (set of vertices is a list, but the order is not fixed) - expected_vertex_values = np.array( - [ - [-0.0, -0.0, -0.0, -0.0], - [-0.0, 1.0, -0.0, 1.0], - [-0.0, -0.0, 1.0, 1.0], - [0.0, 1.0, 1.0, 1.0], - ] - ) - - assert np.isclose(sum([o.value for o in P.objectives]), 0.005) - assert np.isclose(P.expr.edge_activates.value, expected_edge_values).all() - assert np.isclose(np.sum(P.expr.vertex_value.value, axis=0), expected_vertex_values.sum(axis=0)).all() diff --git a/tests/methods/signaling/test_cellnopt_plotting.py b/tests/methods/signaling/test_cellnopt_plotting.py new file mode 100644 index 000000000..ecfe0004a --- /dev/null +++ b/tests/methods/signaling/test_cellnopt_plotting.py @@ -0,0 +1,269 @@ +from pathlib import Path + +import nbformat +import numpy as np +import pytest +from nbclient import NotebookClient + +from corneto._plotting import to_dot_source +from corneto.graph import Graph +from corneto.methods.signaling import ( + CellNOptDAG, + plot_cellnopt_fit, + plot_cellnopt_model, +) +from corneto.methods.signaling.cellnopt_plotting import ( + _build_cellnopt_model_plot, +) + + +def _solve(backend, graph, *, inputs, measurements, inhibitors=None, lambda_reg=1e-3): + method = CellNOptDAG(lambda_reg=lambda_reg, backend=backend) + problem = method.build_many( + graph, + inputs=inputs, + measurements=measurements, + inhibitors=inhibitors, + ) + result = problem.solve() + assert result.status == "optimal" + return method, problem + + +def test_model_plot_expands_and_preserves_or_and_inhibition(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "AND1"), + ("B", -1, "AND1"), + ("AND1", 1, "Y"), + ("C", 1, "Y"), + ] + ) + method, problem = _solve( + backend, + graph, + inputs={ + "and_route": {"A": 1, "B": 0, "C": 0}, + "or_route": {"A": 0, "B": 0, "C": 1}, + }, + measurements={ + "and_route": {"Y": 1}, + "or_route": {"Y": 1}, + }, + ) + + spec = _build_cellnopt_model_plot(method, problem, width_by="flow") + and_nodes = [vertex for vertex in spec.graph.V if str(vertex).startswith("__cellnopt_and_")] + assert len(and_nodes) == 1 + and_node = and_nodes[0] + assert any(source == frozenset({"C"}) and target == frozenset({"Y"}) for source, target in spec.graph.E) + assert any(source == frozenset({"A"}) and target == frozenset({and_node}) for source, target in spec.graph.E) + assert any(source == frozenset({and_node}) and target == frozenset({"Y"}) for source, target in spec.graph.E) + + negative_edges = [ + index for index, attrs in enumerate(spec.graph.get_attr_edges()) if attrs.get("interaction") == -1 + ] + assert len(negative_edges) == 1 + assert spec.edge_attributes[negative_edges[0]]["arrowhead"] == "tee" + assert spec.edge_attributes[negative_edges[0]]["color"] == "#C43C39" + aggregate_edges = [ + attrs for attrs in spec.edge_attributes.values() if "aggregate structural flow" in attrs["tooltip"] + ] + assert len(aggregate_edges) == 1 + assert np.isfinite(float(aggregate_edges[0]["penwidth"])) + + dot = to_dot_source( + spec.graph, + graph_attr=spec.graph_attributes, + node_attr=spec.node_attributes, + custom_edge_attr=spec.edge_attributes, + custom_vertex_attr=spec.vertex_attributes, + ) + assert 'label="AND"' in dot + assert 'arrowhead="tee"' in dot + assert dot.count('-> "Y"') == 2 + + +def test_condition_plot_distinguishes_active_and_selected_inactive_reactions(backend): + graph = Graph.from_tuples([("A", 1, "Y")]) + method, problem = _solve( + backend, + graph, + inputs={"off": {"A": 0}, "on": {"A": 1}}, + measurements={"off": {"Y": 0}, "on": {"Y": 1}}, + ) + + off = _build_cellnopt_model_plot(method, problem, condition="off") + on = _build_cellnopt_model_plot(method, problem, condition="on") + hidden = _build_cellnopt_model_plot( + method, + problem, + condition="off", + show_inactive=False, + ) + + assert off.edge_attributes[0]["style"] == "dashed" + assert "active=0" in off.edge_attributes[0]["tooltip"] + assert on.edge_attributes[0]["style"] == "solid" + assert "active=1" in on.edge_attributes[0]["tooltip"] + assert hidden.graph.num_edges == 0 + assert set(hidden.graph.V) == {"A", "Y"} + + +def test_model_plot_can_reveal_unselected_reactions(backend): + graph = Graph.from_tuples( + [ + ("A", 1, "Y"), + ("C", 1, "D"), + ] + ) + method, problem = _solve( + backend, + graph, + inputs={"condition": {"A": 1, "C": 1}}, + measurements={"condition": {"Y": 1}}, + ) + + selected = _build_cellnopt_model_plot(method, problem) + complete = _build_cellnopt_model_plot(method, problem, show_unselected=True) + + assert selected.graph.num_edges == 1 + assert complete.graph.num_edges == 2 + unselected = [attrs for attrs in complete.edge_attributes.values() if "selected=0" in attrs["tooltip"]] + assert len(unselected) == 1 + assert unselected[0]["style"] == "dotted" + + +def test_fit_views_support_uneven_measurements_and_named_subsets(backend): + pytest.importorskip("matplotlib") + graph = Graph.from_tuples( + [ + ("A", 1, "Y"), + ("A", 1, "Z"), + ] + ) + method, problem = _solve( + backend, + graph, + inputs={ + "very_long_enabled_condition": {"A": 1}, + "blocked": {"A": 1}, + }, + inhibitors={ + "very_long_enabled_condition": {}, + "blocked": {"Y": 1}, + }, + measurements={ + "very_long_enabled_condition": {"Y": 0.8}, + "blocked": {"Z": 1}, + }, + ) + + grid_figure, grid_axes = plot_cellnopt_fit(method, problem) + inferred_figure, inferred_axes = plot_cellnopt_fit( + method, + problem, + signals=["A"], + ) + heatmap_figure, heatmap_axes = plot_cellnopt_fit( + method, + problem, + view="heatmap", + conditions=["blocked"], + signals=["Z"], + ) + + assert grid_axes.shape == (2, 3) + assert inferred_axes.shape == (2, 2) + assert heatmap_axes.shape == (4,) + assert grid_figure is grid_axes[0, 0].figure + assert inferred_figure is inferred_axes[0, 0].figure + assert heatmap_figure is heatmap_axes[0].figure + + measured_axis = grid_axes[0, 0] + assert [line.get_label() for line in measured_axis.lines] == [ + "_nolegend_", + "Model", + "Observed", + ] + assert all(np.allclose(line.get_xdata(), 0) for line in measured_axis.lines) + assert np.allclose(measured_axis.lines[-2].get_ydata(), [1]) + assert np.allclose(measured_axis.lines[-1].get_ydata(), [0.8]) + assert [tick.get_text() for tick in grid_axes[-1, 0].get_xticklabels()] == ["Endpoint"] + + prediction_only_axis = grid_axes[0, 1] + assert [line.get_label() for line in prediction_only_axis.lines] == ["Model"] + assert all([line.get_label() for line in inferred_axes[row, 0].lines] == ["Model"] for row in range(2)) + assert grid_axes[1, 0].patch.get_hatch() == "//" + assert prediction_only_axis.patch.get_hatch() == "//" + + +def test_fit_grid_handles_one_condition_and_one_signal(backend): + pytest.importorskip("matplotlib") + method, problem = _solve( + backend, + Graph.from_tuples([("A", 1, "Y")]), + inputs={"single_endpoint": {"A": 1}}, + measurements={"single_endpoint": {"Y": 0.2}}, + ) + + figure, axes = plot_cellnopt_fit(method, problem) + + assert axes.shape == (1, 2) + assert figure is axes[0, 0].figure + assert [tick.get_text() for tick in axes[0, 0].get_xticklabels()] == ["Endpoint"] + assert [line.get_label() for line in axes[0, 0].lines] == [ + "_nolegend_", + "Model", + "Observed", + ] + assert np.allclose(axes[0, 0].lines[-2].get_ydata(), [0]) + assert np.allclose(axes[0, 0].lines[-1].get_ydata(), [0.2]) + + +def test_networkx_model_plot_returns_real_figure(backend): + pytest.importorskip("matplotlib") + pytest.importorskip("networkx") + method, problem = _solve( + backend, + Graph.from_tuples([("A", -1, "Y")]), + inputs={"condition": {"A": 0}}, + measurements={"condition": {"Y": 1}}, + ) + + figure = plot_cellnopt_model(method, problem, renderer="networkx") + + assert len(figure.axes) == 1 + assert {"A", "Y"} <= {text.get_text() for text in figure.axes[0].texts} + assert len(figure.axes[0].patches) == 1 + + +def test_plotting_requires_a_solved_problem(): + method = CellNOptDAG(lambda_reg=0) + method.build( + Graph.from_tuples([("A", 1, "Y")]), + inputs={"A": 1}, + measurements={"Y": 1}, + ) + + with pytest.raises(ValueError, match="Solve a feasible problem"): + plot_cellnopt_model(method, renderer="networkx") + with pytest.raises(ValueError, match="Solve a feasible problem"): + plot_cellnopt_fit(method) + + +def test_cellnopt_worked_notebook_executes_every_cell(): + notebook_path = Path(__file__).parents[3] / "docs/guide/signaling/cellnopt_dag.ipynb" + notebook = nbformat.read(notebook_path, as_version=4) + + executed = NotebookClient( + notebook, + timeout=300, + kernel_name="python3", + resources={"metadata": {"path": str(notebook_path.parent)}}, + ).execute() + + errors = [ + output for cell in executed.cells for output in cell.get("outputs", []) if output.get("output_type") == "error" + ] + assert errors == [] diff --git a/tests/methods/test_user_inputs.py b/tests/methods/test_user_inputs.py index 4093c3717..10edfde9b 100644 --- a/tests/methods/test_user_inputs.py +++ b/tests/methods/test_user_inputs.py @@ -102,13 +102,32 @@ def test_imat_separates_objectives_and_expression(backend): } gene_method = MultiSampleIMAT(backend=backend) - gene_method.build(model, gene_expression={"G1": 2}, objectives={"R1": -1}) + gene_method.build( + model, + gene_expression={"G1": 2}, + objectives={"R1": -1}, + reaction_bounds={"R1": (1, 8)}, + ) gene_feature = next( feature for feature in gene_method.processed_data.samples["condition"].features if feature.id == "R1" ) assert gene_feature.data["role"] == "objective" assert gene_feature.data["imat_score"] == 2.0 + bounded_gene_method = MultiSampleIMAT(backend=backend) + bounded_gene_method.build( + model, + gene_expression={"G1": 2}, + reaction_bounds={"R1": (1, 8)}, + ) + bounded_gene_feature = next( + feature for feature in bounded_gene_method.processed_data.samples["condition"].features if feature.id == "R1" + ) + assert bounded_gene_feature.data["role"] == "expression" + assert bounded_gene_feature.data["value"] == 2.0 + assert bounded_gene_feature.data["lower_bound"] == 1.0 + assert bounded_gene_feature.data["upper_bound"] == 8.0 + many = MultiSampleIMAT(backend=backend) many.build_many( model, diff --git a/tests/test_api_contract.py b/tests/test_api_contract.py index 21fc32036..900fc0092 100644 --- a/tests/test_api_contract.py +++ b/tests/test_api_contract.py @@ -79,6 +79,12 @@ def test_methods_api_surface_and_identity(): assert methods.create_multisample_shortest_path is create_multisample_shortest_path +def test_experimental_cellnopt_ilp_surface_is_removed(): + """The superseded experimental CellNOpt ILP modules have no shims.""" + assert importlib.util.find_spec("corneto.methods.signal") is None + assert importlib.util.find_spec("corneto.methods.signaling.cellnopt_ilp") is None + + def test_ml_api_replaces_private_module(): """KPNN helpers live in the public ML module without a private-path shim.""" assert ml.__all__ == [ diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index d41ab62e8..b9cf74472 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -1,7 +1,5 @@ """Tests for compatibility APIs retained during the CORNETO 1.x line.""" -import importlib - import pytest import corneto as cn @@ -24,14 +22,3 @@ def test_undocumented_backend_compatibility_apis_are_removed(): with pytest.raises(TypeError, match="unexpected keyword argument 'graph'"): ProblemDef(graph=object()) - - -def test_signal_package_warns_and_reexports_canonical_cellnopt(): - """The old CellNOpt path warns and preserves function identity.""" - canonical = importlib.import_module("corneto.methods.signaling.cellnopt_ilp") - - with pytest.warns(FutureWarning, match=r"corneto\.methods\.signal is deprecated"): - legacy = importlib.import_module("corneto.methods.signal.cellnopt_ilp") - - assert legacy.cellnoptILP is canonical.cellnoptILP - assert legacy.expand_graph_for_flows is canonical.expand_graph_for_flows diff --git a/tests/test_generate_switcher.py b/tests/test_generate_switcher.py new file mode 100644 index 000000000..0df9b6794 --- /dev/null +++ b/tests/test_generate_switcher.py @@ -0,0 +1,24 @@ +"""Tests for the public documentation version switcher.""" + +from scripts.generate_switcher import build_entries + + +def test_switcher_lists_stable_and_release_tags_without_dev_docs(monkeypatch): + """The public trunk has no separate latest/development documentation site.""" + monkeypatch.setattr("scripts.generate_switcher._get_tags", lambda: ["v1.0.0-rc.2"]) + + entries = build_entries("https://corneto.org") + + assert entries == [ + { + "name": "stable", + "version": "stable", + "url": "https://corneto.org/stable/", + "preferred": True, + }, + { + "name": "v1.0.0-rc.2", + "version": "v1.0.0-rc.2", + "url": "https://corneto.org/v1.0.0-rc.2/", + }, + ] diff --git a/tests/test_release.py b/tests/test_release.py index 072020055..69b76d2ed 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -17,7 +17,6 @@ def test_dry_run_uses_selected_remote(monkeypatch, capsys): monkeypatch.setattr(release, "_ensure_clean_tree", lambda: None) monkeypatch.setattr(release, "_ensure_on_main", lambda: None) monkeypatch.setattr(release, "_ensure_up_to_date_with_remote_main", calls.append) - monkeypatch.setattr(release, "_ensure_dev_is_merged", calls.append) monkeypatch.setattr( release, "_ensure_tag_does_not_exist", @@ -28,7 +27,6 @@ def test_dry_run_uses_selected_remote(monkeypatch, capsys): assert result == 0 assert calls == [ - "public", "public", "public", ("v1.0.0-beta.8", "public"),