viva-copasi 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vivarium Collective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: viva-copasi
3
+ Version: 0.1.2
4
+ Summary: process-bigraph-compatible COPASI Steps and Processes (UTC + SteadyState)
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: process-bigraph>=1.8.5
10
+ Requires-Dist: bigraph-schema
11
+ Requires-Dist: copasi-basico
12
+ Requires-Dist: python-copasi
13
+ Requires-Dist: viva-superpowers
14
+ Requires-Dist: pyyaml
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: bigraph-viz; extra == "dev"
18
+ Requires-Dist: matplotlib; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # viva-copasi
22
+
23
+ process-bigraph-compatible COPASI Steps and Processes for SBML model simulation.
24
+
25
+ Exposes any SBML kinetic model as a `process-bigraph` Step or Process so it can
26
+ be composed with other simulators in a bigraph document, using
27
+ [COPASI](https://copasi.org/) / [basico](https://basico.readthedocs.io/) as the
28
+ simulation back-end.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ uv venv .venv && source .venv/bin/activate
34
+ uv pip install -e .
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from importlib.resources import files
41
+ from process_bigraph import Composite, gather_emitter_results
42
+ from viva_copasi.composites import build_composite, register_copasi
43
+
44
+ # Resolve the bundled Repressilator model
45
+ model_path = str(files('viva_copasi.composites').joinpath('repressilator.xml'))
46
+
47
+ # Build and run the incremental UTC process composite
48
+ sim = build_composite('utc-process')
49
+ for _ in range(10):
50
+ sim.update({}, 1.0) # advance 1 time unit per step
51
+
52
+ results = gather_emitter_results(sim)
53
+ print(results[('emitter',)][-1])
54
+ ```
55
+
56
+ ## Classes
57
+
58
+ - `CopasiUTCStep` — one-shot Uniform Time Course (UTC) trajectory (Step)
59
+ - `CopasiUTCProcess` — incremental UTC simulation (Process)
60
+ - `CopasiSteadyStateStep` — steady-state solve (Step)
61
+ - `BaseCopasi` — shared model-load mixin (SBML parsing, species/reaction indexing)
62
+
63
+ All classes accept a `model_source` config key pointing to an SBML or COPASI
64
+ model file (path or URL).
65
+
66
+ ## Composites
67
+
68
+ Three discoverable composites in `viva_copasi/composites/`:
69
+
70
+ | Name | Type | Description |
71
+ |---|---|---|
72
+ | `utc-step` | Step | One-shot UTC trajectory; returns full time series in a single `update()` call |
73
+ | `utc-process` | Process | Incremental UTC; advances one interval per `update()`, emitting concentrations + fluxes |
74
+ | `steady-state` | Step | Steady-state solve; returns equilibrium concentrations and reaction fluxes |
75
+
76
+ Each ships with the bundled SBML model
77
+ [BIOMD0000000012](https://www.ebi.ac.uk/biomodels/BIOMD0000000012) — the Elowitz
78
+ & Leibler 2000 Repressilator.
79
+
80
+ Load a composite by name:
81
+
82
+ ```python
83
+ from viva_copasi.composites import build_composite, list_composite_specs
84
+
85
+ print(list_composite_specs()) # ['steady-state', 'utc-process', 'utc-step']
86
+ sim = build_composite('utc-process')
87
+ ```
88
+
89
+ Override the model or parameters:
90
+
91
+ ```python
92
+ sim = build_composite('utc-step', overrides={
93
+ 'model_source': '/path/to/my_model.xml',
94
+ 'time': 50.0,
95
+ 'n_points': 51,
96
+ })
97
+ ```
98
+
99
+ ## API
100
+
101
+ ### `CopasiUTCStep` (Step)
102
+
103
+ One-shot UTC trajectory.
104
+
105
+ Config: `model_source` (str), `time` (float, default 1.0), `n_points` (int, default 2).
106
+
107
+ Outputs: `result` — dict with keys `time` (list), `columns` (list), `values` (list of rows).
108
+
109
+ ### `CopasiUTCProcess` (Process)
110
+
111
+ Incremental UTC simulation.
112
+
113
+ Config: `model_source` (str), `time` (float, default 1.0), `intervals` (int, default 10).
114
+
115
+ Outputs: `species_concentrations` (map[float]), `fluxes` (map[float]), `time` (list[float]).
116
+
117
+ ### `CopasiSteadyStateStep` (Step)
118
+
119
+ Steady-state solve.
120
+
121
+ Config: `model_source` (str).
122
+
123
+ Outputs: `results` — dict with keys `time`, `species_concentrations`, `fluxes`.
124
+
125
+ ### `register_copasi(core=None)`
126
+
127
+ Register all COPASI process classes, the RAM emitter, and the
128
+ `SpeciesConcentrationsPlot` visualization into a bigraph core.
129
+
130
+ ### `make_copasi_utc_document(...)` / `make_copasi_utc_process_document(...)` / `make_copasi_steady_state_document(...)`
131
+
132
+ Programmatic composite-document factories for callers that want full control
133
+ over wiring without a YAML file.
134
+
135
+ ## Tests
136
+
137
+ ```bash
138
+ python -m pytest tests/ -q
139
+ ```
140
+
141
+ ## Demos
142
+
143
+ ```bash
144
+ python demo/demo_report.py # runs all 3 classes; saves demo/report.html
145
+ python demo/composite_report.py # runs utc-process composite; saves demo/utc-process-report.html
146
+ ```
@@ -0,0 +1,126 @@
1
+ # viva-copasi
2
+
3
+ process-bigraph-compatible COPASI Steps and Processes for SBML model simulation.
4
+
5
+ Exposes any SBML kinetic model as a `process-bigraph` Step or Process so it can
6
+ be composed with other simulators in a bigraph document, using
7
+ [COPASI](https://copasi.org/) / [basico](https://basico.readthedocs.io/) as the
8
+ simulation back-end.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ uv venv .venv && source .venv/bin/activate
14
+ uv pip install -e .
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ from importlib.resources import files
21
+ from process_bigraph import Composite, gather_emitter_results
22
+ from viva_copasi.composites import build_composite, register_copasi
23
+
24
+ # Resolve the bundled Repressilator model
25
+ model_path = str(files('viva_copasi.composites').joinpath('repressilator.xml'))
26
+
27
+ # Build and run the incremental UTC process composite
28
+ sim = build_composite('utc-process')
29
+ for _ in range(10):
30
+ sim.update({}, 1.0) # advance 1 time unit per step
31
+
32
+ results = gather_emitter_results(sim)
33
+ print(results[('emitter',)][-1])
34
+ ```
35
+
36
+ ## Classes
37
+
38
+ - `CopasiUTCStep` — one-shot Uniform Time Course (UTC) trajectory (Step)
39
+ - `CopasiUTCProcess` — incremental UTC simulation (Process)
40
+ - `CopasiSteadyStateStep` — steady-state solve (Step)
41
+ - `BaseCopasi` — shared model-load mixin (SBML parsing, species/reaction indexing)
42
+
43
+ All classes accept a `model_source` config key pointing to an SBML or COPASI
44
+ model file (path or URL).
45
+
46
+ ## Composites
47
+
48
+ Three discoverable composites in `viva_copasi/composites/`:
49
+
50
+ | Name | Type | Description |
51
+ |---|---|---|
52
+ | `utc-step` | Step | One-shot UTC trajectory; returns full time series in a single `update()` call |
53
+ | `utc-process` | Process | Incremental UTC; advances one interval per `update()`, emitting concentrations + fluxes |
54
+ | `steady-state` | Step | Steady-state solve; returns equilibrium concentrations and reaction fluxes |
55
+
56
+ Each ships with the bundled SBML model
57
+ [BIOMD0000000012](https://www.ebi.ac.uk/biomodels/BIOMD0000000012) — the Elowitz
58
+ & Leibler 2000 Repressilator.
59
+
60
+ Load a composite by name:
61
+
62
+ ```python
63
+ from viva_copasi.composites import build_composite, list_composite_specs
64
+
65
+ print(list_composite_specs()) # ['steady-state', 'utc-process', 'utc-step']
66
+ sim = build_composite('utc-process')
67
+ ```
68
+
69
+ Override the model or parameters:
70
+
71
+ ```python
72
+ sim = build_composite('utc-step', overrides={
73
+ 'model_source': '/path/to/my_model.xml',
74
+ 'time': 50.0,
75
+ 'n_points': 51,
76
+ })
77
+ ```
78
+
79
+ ## API
80
+
81
+ ### `CopasiUTCStep` (Step)
82
+
83
+ One-shot UTC trajectory.
84
+
85
+ Config: `model_source` (str), `time` (float, default 1.0), `n_points` (int, default 2).
86
+
87
+ Outputs: `result` — dict with keys `time` (list), `columns` (list), `values` (list of rows).
88
+
89
+ ### `CopasiUTCProcess` (Process)
90
+
91
+ Incremental UTC simulation.
92
+
93
+ Config: `model_source` (str), `time` (float, default 1.0), `intervals` (int, default 10).
94
+
95
+ Outputs: `species_concentrations` (map[float]), `fluxes` (map[float]), `time` (list[float]).
96
+
97
+ ### `CopasiSteadyStateStep` (Step)
98
+
99
+ Steady-state solve.
100
+
101
+ Config: `model_source` (str).
102
+
103
+ Outputs: `results` — dict with keys `time`, `species_concentrations`, `fluxes`.
104
+
105
+ ### `register_copasi(core=None)`
106
+
107
+ Register all COPASI process classes, the RAM emitter, and the
108
+ `SpeciesConcentrationsPlot` visualization into a bigraph core.
109
+
110
+ ### `make_copasi_utc_document(...)` / `make_copasi_utc_process_document(...)` / `make_copasi_steady_state_document(...)`
111
+
112
+ Programmatic composite-document factories for callers that want full control
113
+ over wiring without a YAML file.
114
+
115
+ ## Tests
116
+
117
+ ```bash
118
+ python -m pytest tests/ -q
119
+ ```
120
+
121
+ ## Demos
122
+
123
+ ```bash
124
+ python demo/demo_report.py # runs all 3 classes; saves demo/report.html
125
+ python demo/composite_report.py # runs utc-process composite; saves demo/utc-process-report.html
126
+ ```
@@ -0,0 +1,80 @@
1
+ """Back-compat shim: ``pbg_copasi`` was renamed to ``viva_copasi``.
2
+
3
+ Part of the pbg -> viva rebrand (the GitHub repo is already
4
+ ``vivarium-collective/viva-copasi``). This shim keeps every existing consumer
5
+ working during the deprecation window:
6
+
7
+ * ``import pbg_copasi`` / ``from pbg_copasi import X`` works (re-exports
8
+ the new package's top-level ``__all__``);
9
+ * ``import pbg_copasi.<sub>`` transparently resolves to
10
+ ``viva_copasi.<sub>`` via a meta-path finder (so
11
+ ``from pbg_copasi.simulators import ...`` still resolves); and
12
+ * ``python -m pbg_copasi.<sub>`` still executes.
13
+
14
+ Importing anything under this package emits a one-time
15
+ :class:`DeprecationWarning`. Update imports to ``viva_copasi``; this shim is
16
+ removed in a future major release.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import importlib
21
+ import importlib.abc
22
+ import importlib.util
23
+ import sys
24
+ import warnings
25
+
26
+ warnings.warn(
27
+ "pbg_copasi is renamed to viva_copasi; update your imports "
28
+ "(the pbg_copasi alias is removed in a future major release).",
29
+ DeprecationWarning,
30
+ stacklevel=2,
31
+ )
32
+
33
+ _OLD = "pbg_copasi."
34
+ _NEW = "viva_copasi."
35
+
36
+
37
+ class _Redirect(importlib.abc.MetaPathFinder, importlib.abc.Loader):
38
+ """Forward ``pbg_copasi.<sub>`` imports to ``viva_copasi.<sub>``."""
39
+
40
+ def _target(self, name: str) -> str:
41
+ return _NEW + name[len(_OLD):]
42
+
43
+ def find_spec(self, name, path=None, target=None):
44
+ if not name.startswith(_OLD):
45
+ return None
46
+ real = importlib.util.find_spec(self._target(name))
47
+ if real is None:
48
+ return None
49
+ spec = importlib.util.spec_from_loader(
50
+ name,
51
+ self,
52
+ origin=real.origin,
53
+ is_package=real.submodule_search_locations is not None,
54
+ )
55
+ if real.submodule_search_locations is not None:
56
+ spec.submodule_search_locations = list(real.submodule_search_locations)
57
+ return spec
58
+
59
+ def create_module(self, spec):
60
+ # Alias the fully-initialized new-package module under BOTH names so
61
+ # `import a.b` and identity checks against either name agree.
62
+ mod = importlib.import_module(self._target(spec.name))
63
+ sys.modules[spec.name] = mod
64
+ return mod
65
+
66
+ def exec_module(self, module): # already executed by import_module
67
+ pass
68
+
69
+ def get_code(self, name):
70
+ # Support `python -m pbg_copasi.<sub>`: runpy needs a code object.
71
+ target = self._target(name)
72
+ return importlib.util.find_spec(target).loader.get_code(target)
73
+
74
+
75
+ sys.meta_path.insert(0, _Redirect())
76
+
77
+ _new = importlib.import_module("viva_copasi")
78
+ __version__ = getattr(_new, "__version__", "0.0.0")
79
+ # Re-export the new package's public surface (if any is declared).
80
+ globals().update({k: getattr(_new, k) for k in getattr(_new, "__all__", [])})
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "viva-copasi"
7
+ version = "0.1.2"
8
+ description = "process-bigraph-compatible COPASI Steps and Processes (UTC + SteadyState)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "process-bigraph>=1.8.5",
14
+ "bigraph-schema",
15
+ "copasi-basico",
16
+ "python-copasi",
17
+ "viva-superpowers",
18
+ "pyyaml",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = ["pytest", "bigraph-viz", "matplotlib"]
23
+
24
+ [tool.uv.sources]
25
+ # viva-superpowers (renamed from pbg-superpowers) is git-only, not on PyPI.
26
+ viva-superpowers = { git = "https://github.com/vivarium-collective/viva-superpowers.git", branch = "main" }
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["viva_copasi*", "pbg_copasi"] # pbg_copasi = deprecation shim
30
+
31
+ [tool.setuptools.package-data]
32
+ "viva_copasi.composites" = ["*.xml", "*.yaml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,164 @@
1
+ """Integration tests for viva-copasi composite specs.
2
+
3
+ Tests cover:
4
+ - Discovery: list_composite_specs() returns the three expected names.
5
+ - Structural validity: each *.composite.yaml has the required top-level keys.
6
+ - Instantiation: build_composite() produces a runnable process_bigraph.Composite.
7
+ - Short run: the UTC process composite advances and emits data.
8
+
9
+ Test model: Elowitz 2000 Repressilator (BIOMD0000000012), bundled at
10
+ viva_copasi/composites/repressilator.xml.
11
+ """
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+ from process_bigraph import Composite, allocate_core, gather_emitter_results
16
+ from process_bigraph.emitter import RAMEmitter
17
+
18
+ from viva_copasi.composites import (
19
+ list_composite_specs,
20
+ load_composite_spec,
21
+ build_composite,
22
+ register_copasi,
23
+ make_copasi_utc_process_document,
24
+ make_copasi_steady_state_document,
25
+ _COMPOSITES_DIR,
26
+ )
27
+ from viva_copasi.processes import (
28
+ CopasiUTCStep,
29
+ CopasiSteadyStateStep,
30
+ CopasiUTCProcess,
31
+ )
32
+
33
+ # Repressilator model — same file bundled in the composites package dir.
34
+ BUNDLED_MODEL = str(_COMPOSITES_DIR / "repressilator.xml")
35
+
36
+
37
+ @pytest.fixture
38
+ def core():
39
+ """Minimal core with all COPASI classes + RAM emitter registered."""
40
+ return register_copasi()
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Discovery
45
+ # ---------------------------------------------------------------------------
46
+
47
+ def test_list_composite_specs_returns_three():
48
+ """Discovery returns the three expected composite names."""
49
+ specs = list_composite_specs()
50
+ assert 'utc-step' in specs
51
+ assert 'utc-process' in specs
52
+ assert 'steady-state' in specs
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Structural validity
57
+ # ---------------------------------------------------------------------------
58
+
59
+ def test_utc_step_spec_well_formed():
60
+ """utc-step.composite.yaml has required top-level keys."""
61
+ spec = load_composite_spec('utc-step')
62
+ assert 'name' in spec
63
+ assert 'state' in spec
64
+ assert spec['name'] == 'utc-step'
65
+ assert 'copasi_utc' in spec['state']
66
+ assert spec['state']['copasi_utc']['address'] == 'local:CopasiUTCStep'
67
+
68
+
69
+ def test_utc_process_spec_well_formed():
70
+ """utc-process.composite.yaml has required top-level keys."""
71
+ spec = load_composite_spec('utc-process')
72
+ assert 'name' in spec
73
+ assert 'state' in spec
74
+ assert spec['name'] == 'utc-process'
75
+ assert 'copasi_utc_process' in spec['state']
76
+ assert spec['state']['copasi_utc_process']['address'] == 'local:CopasiUTCProcess'
77
+
78
+
79
+ def test_steady_state_spec_well_formed():
80
+ """steady-state.composite.yaml has required top-level keys."""
81
+ spec = load_composite_spec('steady-state')
82
+ assert 'name' in spec
83
+ assert 'state' in spec
84
+ assert spec['name'] == 'steady-state'
85
+ assert 'copasi_ss' in spec['state']
86
+ assert spec['state']['copasi_ss']['address'] == 'local:CopasiSteadyStateStep'
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # build_composite — instantiation
91
+ # ---------------------------------------------------------------------------
92
+
93
+ def test_build_composite_utc_step(core):
94
+ """build_composite('utc-step') instantiates a Composite without error."""
95
+ sim = build_composite('utc-step', core=core)
96
+ assert sim is not None
97
+ assert isinstance(sim, Composite)
98
+
99
+
100
+ def test_build_composite_utc_process(core):
101
+ """build_composite('utc-process') instantiates a Composite without error."""
102
+ sim = build_composite('utc-process', core=core)
103
+ assert sim is not None
104
+ assert isinstance(sim, Composite)
105
+
106
+
107
+ def test_build_composite_steady_state(core):
108
+ """build_composite('steady-state') instantiates a Composite without error."""
109
+ sim = build_composite('steady-state', core=core)
110
+ assert sim is not None
111
+ assert isinstance(sim, Composite)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # End-to-end: UTC process composite — short run + emitter check
116
+ # ---------------------------------------------------------------------------
117
+
118
+ def test_utc_process_composite_short_run(core):
119
+ """CopasiUTCProcess composite runs 2 steps and emits species_concentrations."""
120
+ doc = make_copasi_utc_process_document(
121
+ model_source=BUNDLED_MODEL,
122
+ time=1.0,
123
+ intervals=5,
124
+ )
125
+ sim = Composite({'state': doc}, core=core)
126
+ sim.run(2.0)
127
+
128
+ stores = sim.state['stores']
129
+ assert 'species_concentrations' in stores
130
+ assert isinstance(stores['species_concentrations'], dict)
131
+ # Repressilator has 6 species
132
+ assert len(stores['species_concentrations']) >= 3
133
+
134
+
135
+ def test_utc_process_composite_emitter(core):
136
+ """Emitter collects at least one record after a short run."""
137
+ doc = make_copasi_utc_process_document(
138
+ model_source=BUNDLED_MODEL,
139
+ time=1.0,
140
+ intervals=5,
141
+ )
142
+ sim = Composite({'state': doc}, core=core)
143
+ sim.run(2.0)
144
+
145
+ raw = gather_emitter_results(sim)
146
+ rows = raw[('emitter',)]
147
+ assert len(rows) >= 1
148
+ # Each row should have species_concentrations
149
+ row = rows[0]
150
+ assert 'species_concentrations' in row or 'fluxes' in row
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # End-to-end: steady-state composite — single step
155
+ # ---------------------------------------------------------------------------
156
+
157
+ def test_steady_state_composite_runs(core):
158
+ """CopasiSteadyStateStep composite runs and populates results store."""
159
+ doc = make_copasi_steady_state_document(model_source=BUNDLED_MODEL)
160
+ sim = Composite({'state': doc}, core=core)
161
+ sim.run(1.0)
162
+
163
+ ss_stores = sim.state['ss_stores']
164
+ assert 'results' in ss_stores
@@ -0,0 +1,29 @@
1
+ import pytest
2
+
3
+ from viva_copasi.processes import _model_path_resolution
4
+
5
+
6
+ def test_missing_relative_path_raises_with_context(tmp_path, monkeypatch):
7
+ monkeypatch.chdir(tmp_path)
8
+ with pytest.raises(FileNotFoundError) as e:
9
+ _model_path_resolution("does_not_exist.cps")
10
+ msg = str(e.value)
11
+ assert "does_not_exist.cps" in msg
12
+ assert str(tmp_path / "does_not_exist.cps") in msg
13
+ assert str(tmp_path) in msg
14
+
15
+
16
+ def test_missing_absolute_path_raises(tmp_path):
17
+ with pytest.raises(FileNotFoundError):
18
+ _model_path_resolution(str(tmp_path / "nope.xml"))
19
+
20
+
21
+ def test_url_passes_through():
22
+ url = "https://example.org/model.xml"
23
+ assert _model_path_resolution(url) == url
24
+
25
+
26
+ def test_existing_absolute_path_unchanged(tmp_path):
27
+ f = tmp_path / "m.xml"
28
+ f.write_text("<sbml/>")
29
+ assert _model_path_resolution(str(f)) == str(f)