crossbridge 0.1.0__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 Computational Physiology at Simula Research Laboratory
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,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: crossbridge
3
+ Version: 0.1.0
4
+ Summary: Cardiac crossbridge models
5
+ Author-email: Henrik Finsberg <henriknf@simula.no>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Requires-Dist: scipy
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest; extra == "test"
14
+ Requires-Dist: pytest-cov; extra == "test"
15
+ Provides-Extra: demos
16
+ Requires-Dist: matplotlib; extra == "demos"
17
+ Requires-Dist: zero-mech; extra == "demos"
18
+ Requires-Dist: gotranx; extra == "demos"
19
+ Requires-Dist: scipy; extra == "demos"
20
+ Requires-Dist: tqdm; extra == "demos"
21
+ Requires-Dist: numba; extra == "demos"
22
+ Provides-Extra: docs
23
+ Requires-Dist: jupyter-book<2.0; extra == "docs"
24
+ Requires-Dist: jupytext; extra == "docs"
25
+ Requires-Dist: crossbridge[demos]; extra == "docs"
26
+ Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
27
+ Provides-Extra: all
28
+ Requires-Dist: crossbridge[test]; extra == "all"
29
+ Requires-Dist: crossbridge[demos]; extra == "all"
30
+ Requires-Dist: crossbridge[docs]; extra == "all"
31
+ Dynamic: license-file
32
+
33
+ # crossbridge
34
+
35
+ **crossbridge** is a highly efficient, vectorized Python library for simulating cardiac myofilament activation and crossbridge dynamics.
36
+
37
+ ## The Mathematical Models
38
+ `crossbridge` implements several reduced-order models of cardiac myofilament activation
39
+ ([RDQ18](docs/models/rdq18.md), [RDQ20-MF](docs/models/rdq20mf.md), [Land2017](docs/models/land2017.md),
40
+ [Lewalle2024](docs/models/lewalle2024.md)), all sharing a common interface (see "Choosing a Model"
41
+ below) so that a coupled electromechanics simulation can swap between them with minimal code
42
+ changes. See [docs/models](docs/models/index.md) for a description and reference for each model.
43
+
44
+ ## Installation
45
+
46
+ The package requires Python 3.11+. You can install the base package and its dependencies using `pip`.
47
+
48
+ To install the library from the source code:
49
+ ```bash
50
+ git clone https://github.com/ComputationalPhysiology/crossbridge.git
51
+ cd crossbridge
52
+ pip install .
53
+ ```
54
+
55
+ To install with optional dependencies (for running demos, tests, or building docs):
56
+ ```bash
57
+ pip install ".[demos]" # Installs scipy, matplotlib, gotranx, numba, zero-mech, etc.
58
+ pip install ".[test]" # Installs pytest and coverage tools
59
+ pip install ".[docs]" # Installs jupyter-book and sphinx plugins
60
+ pip install ".[all]" # Installs everything
61
+ ```
62
+
63
+ ## Basic Usage
64
+ To most basic usage its to solve for a single cell. The `RDQ18` class provides an `advance_ODE` method that takes in the time step, calcium concentration, and sarcomere length to update the internal state of the model. This can then be used to compute an active tension based on the fraction of permissive crossbridges.
65
+
66
+ ```python
67
+ import numpy as np
68
+ from crossbridge import RDQ18, calcium_trace, sl_trace
69
+
70
+ # Initialize a model with 1000 cells/integration points
71
+ num_cells = 1
72
+ dt = 0.01
73
+ dt_sarc = 2.5e-5
74
+ sarcomere = RDQ18(num_cells=num_cells, Ta_max=60.0, params={"dt": dt_sarc})
75
+
76
+ # Inputs
77
+ t = np.arange(0, 1, dt) # Time array [s]
78
+ Ca = calcium_trace(t) # Calcium transient [uM]
79
+ SL = sl_trace(t) # Sarcomere length transient [um]
80
+
81
+
82
+ Ta = np.zeros(len(t))
83
+ # Advance the ODEs by one time step
84
+ for i, (Cai, SLi) in enumerate(zip(Ca, SL)):
85
+ sarcomere.advance_ODE(dt, Cai, np.array([SLi]))
86
+
87
+ # Compute the fraction of permissive crossbridges (proxy for active tension)
88
+ permissivity = sarcomere.compute_permissivity()[0]
89
+ active_tension = sarcomere.Ta_max * permissivity
90
+ Ta[i] = active_tension
91
+
92
+ import matplotlib.pyplot as plt
93
+
94
+ fig, ax = plt.subplots(3, 1, sharex=True, figsize=(8, 6))
95
+ ax[0].plot(t, Ca)
96
+ ax[0].set_ylabel("Calcium [uM]")
97
+ ax[1].plot(t, SL)
98
+ ax[1].set_ylabel("Sarcomere Length [um]")
99
+ ax[2].plot(t, Ta)
100
+ ax[2].set_ylabel("Active Tension [kPa]")
101
+ ax[2].set_xlabel("Time [s]")
102
+ fig.tight_layout()
103
+ plt.show()
104
+ ```
105
+ ![Example Output](https://github.com/user-attachments/assets/4d07bea6-9f5d-4aae-a1df-2debe6199999)
106
+
107
+
108
+ ## Coupling to Electrophysiology and Mechanics
109
+ Most cellular and tissue-level simulations will require coupling the `RDQ18` model to electrophysiology and mechanics. The `advance_ODE` method is designed to be called at every time step of a larger simulation loop, allowing the sarcomere dynamics to evolve in response to changing calcium and length conditions.
110
+
111
+ The calcium concentration can be either a scalar (if all cells/integration points are assumed to have the same calcium transient) or an array with one entry per cell/integration point. The sarcomere length can similarly be a scalar or an array. Below is a pseudo-code example of how this coupling might look in a larger simulation loop:
112
+
113
+ ```python
114
+ ...
115
+
116
+ SL = np.full(num_cells, 2.2) # Initial sarcomere length [um]
117
+ for t in time_steps:
118
+ # Compute calcium from electrophysiology model
119
+ Cai = compute_calcium(t)
120
+ sarcomere.advance_ODE(dt, Cai, SL)
121
+ # Compute active tension and update mechanics
122
+ permissivity = sarcomere.compute_permissivity()[0]
123
+ active_tension = sarcomere.Ta_max * permissivity
124
+ # Update mechanics model with new active tension
125
+ # and compute new sarcomere length (SL) based on
126
+ # the mechanical response of the tissue
127
+ SL = compute_new_length(active_tension, SL)
128
+ ...
129
+ ```
130
+
131
+ ## Examples & Demos
132
+
133
+ The `demo/` folder contains several scripts demonstrating how to couple the `crossbridge` model to different physics scales. See [demos](demo/index.md) for a description of each demo and how to run them.
134
+
135
+
136
+ ## Testing and Development
137
+ We use `pytest` for unit testing. To run the test suite and check code coverage:
138
+ ```bash
139
+ pytest
140
+ ```
141
+ To run the pre-commit linters (Ruff and MyPy):
142
+ ```bash
143
+ pre-commit run --all-files
144
+ ```
145
+
146
+ ## License
147
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,115 @@
1
+ # crossbridge
2
+
3
+ **crossbridge** is a highly efficient, vectorized Python library for simulating cardiac myofilament activation and crossbridge dynamics.
4
+
5
+ ## The Mathematical Models
6
+ `crossbridge` implements several reduced-order models of cardiac myofilament activation
7
+ ([RDQ18](docs/models/rdq18.md), [RDQ20-MF](docs/models/rdq20mf.md), [Land2017](docs/models/land2017.md),
8
+ [Lewalle2024](docs/models/lewalle2024.md)), all sharing a common interface (see "Choosing a Model"
9
+ below) so that a coupled electromechanics simulation can swap between them with minimal code
10
+ changes. See [docs/models](docs/models/index.md) for a description and reference for each model.
11
+
12
+ ## Installation
13
+
14
+ The package requires Python 3.11+. You can install the base package and its dependencies using `pip`.
15
+
16
+ To install the library from the source code:
17
+ ```bash
18
+ git clone https://github.com/ComputationalPhysiology/crossbridge.git
19
+ cd crossbridge
20
+ pip install .
21
+ ```
22
+
23
+ To install with optional dependencies (for running demos, tests, or building docs):
24
+ ```bash
25
+ pip install ".[demos]" # Installs scipy, matplotlib, gotranx, numba, zero-mech, etc.
26
+ pip install ".[test]" # Installs pytest and coverage tools
27
+ pip install ".[docs]" # Installs jupyter-book and sphinx plugins
28
+ pip install ".[all]" # Installs everything
29
+ ```
30
+
31
+ ## Basic Usage
32
+ To most basic usage its to solve for a single cell. The `RDQ18` class provides an `advance_ODE` method that takes in the time step, calcium concentration, and sarcomere length to update the internal state of the model. This can then be used to compute an active tension based on the fraction of permissive crossbridges.
33
+
34
+ ```python
35
+ import numpy as np
36
+ from crossbridge import RDQ18, calcium_trace, sl_trace
37
+
38
+ # Initialize a model with 1000 cells/integration points
39
+ num_cells = 1
40
+ dt = 0.01
41
+ dt_sarc = 2.5e-5
42
+ sarcomere = RDQ18(num_cells=num_cells, Ta_max=60.0, params={"dt": dt_sarc})
43
+
44
+ # Inputs
45
+ t = np.arange(0, 1, dt) # Time array [s]
46
+ Ca = calcium_trace(t) # Calcium transient [uM]
47
+ SL = sl_trace(t) # Sarcomere length transient [um]
48
+
49
+
50
+ Ta = np.zeros(len(t))
51
+ # Advance the ODEs by one time step
52
+ for i, (Cai, SLi) in enumerate(zip(Ca, SL)):
53
+ sarcomere.advance_ODE(dt, Cai, np.array([SLi]))
54
+
55
+ # Compute the fraction of permissive crossbridges (proxy for active tension)
56
+ permissivity = sarcomere.compute_permissivity()[0]
57
+ active_tension = sarcomere.Ta_max * permissivity
58
+ Ta[i] = active_tension
59
+
60
+ import matplotlib.pyplot as plt
61
+
62
+ fig, ax = plt.subplots(3, 1, sharex=True, figsize=(8, 6))
63
+ ax[0].plot(t, Ca)
64
+ ax[0].set_ylabel("Calcium [uM]")
65
+ ax[1].plot(t, SL)
66
+ ax[1].set_ylabel("Sarcomere Length [um]")
67
+ ax[2].plot(t, Ta)
68
+ ax[2].set_ylabel("Active Tension [kPa]")
69
+ ax[2].set_xlabel("Time [s]")
70
+ fig.tight_layout()
71
+ plt.show()
72
+ ```
73
+ ![Example Output](https://github.com/user-attachments/assets/4d07bea6-9f5d-4aae-a1df-2debe6199999)
74
+
75
+
76
+ ## Coupling to Electrophysiology and Mechanics
77
+ Most cellular and tissue-level simulations will require coupling the `RDQ18` model to electrophysiology and mechanics. The `advance_ODE` method is designed to be called at every time step of a larger simulation loop, allowing the sarcomere dynamics to evolve in response to changing calcium and length conditions.
78
+
79
+ The calcium concentration can be either a scalar (if all cells/integration points are assumed to have the same calcium transient) or an array with one entry per cell/integration point. The sarcomere length can similarly be a scalar or an array. Below is a pseudo-code example of how this coupling might look in a larger simulation loop:
80
+
81
+ ```python
82
+ ...
83
+
84
+ SL = np.full(num_cells, 2.2) # Initial sarcomere length [um]
85
+ for t in time_steps:
86
+ # Compute calcium from electrophysiology model
87
+ Cai = compute_calcium(t)
88
+ sarcomere.advance_ODE(dt, Cai, SL)
89
+ # Compute active tension and update mechanics
90
+ permissivity = sarcomere.compute_permissivity()[0]
91
+ active_tension = sarcomere.Ta_max * permissivity
92
+ # Update mechanics model with new active tension
93
+ # and compute new sarcomere length (SL) based on
94
+ # the mechanical response of the tissue
95
+ SL = compute_new_length(active_tension, SL)
96
+ ...
97
+ ```
98
+
99
+ ## Examples & Demos
100
+
101
+ The `demo/` folder contains several scripts demonstrating how to couple the `crossbridge` model to different physics scales. See [demos](demo/index.md) for a description of each demo and how to run them.
102
+
103
+
104
+ ## Testing and Development
105
+ We use `pytest` for unit testing. To run the test suite and check code coverage:
106
+ ```bash
107
+ pytest
108
+ ```
109
+ To run the pre-commit linters (Ruff and MyPy):
110
+ ```bash
111
+ pre-commit run --all-files
112
+ ```
113
+
114
+ ## License
115
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,126 @@
1
+ [build-system] # Require setuptool version due to https://github.com/pypa/setuptools/issues/2938
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+
4
+ [project]
5
+ name = "crossbridge"
6
+ version = "0.1.0"
7
+ description = "Cardiac crossbridge models"
8
+ authors = [{ name = "Henrik Finsberg", email = "henriknf@simula.no" }]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ readme = "README.md"
12
+ dependencies = [
13
+ "numpy",
14
+ "scipy"
15
+ ]
16
+ requires-python = ">=3.11"
17
+
18
+
19
+ [project.optional-dependencies]
20
+ test = ["pytest", "pytest-cov"]
21
+ demos = ["matplotlib", "zero-mech", "gotranx", "scipy", "tqdm", "numba"]
22
+ docs = [
23
+ "jupyter-book<2.0",
24
+ "jupytext",
25
+ "crossbridge[demos]",
26
+ "sphinxcontrib-bibtex",
27
+ ]
28
+ all = [
29
+ "crossbridge[test]",
30
+ "crossbridge[demos]",
31
+ "crossbridge[docs]",
32
+ ]
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+
38
+ [tool.pytest.ini_options]
39
+ addopts = [
40
+ "--cov=crossbridge",
41
+ "--cov-report=html",
42
+ "--cov-report=term-missing",
43
+ "-v",
44
+ ]
45
+
46
+ testpaths = ["tests"]
47
+
48
+ [tool.mypy]
49
+ files = ["src/crossbridge", "tests"]
50
+ ignore_missing_imports = true
51
+ exclude = [
52
+ "docs",
53
+ "examples",
54
+ ]
55
+
56
+
57
+
58
+ [tool.ruff]
59
+
60
+ # Exclude a variety of commonly ignored directories.
61
+ exclude = [
62
+ "examples",
63
+ ".bzr",
64
+ ".direnv",
65
+ ".eggs",
66
+ ".git",
67
+ ".hg",
68
+ ".mypy_cache",
69
+ ".nox",
70
+ ".pants.d",
71
+ ".pytype",
72
+ ".ruff_cache",
73
+ ".svn",
74
+ ".tox",
75
+ ".venv",
76
+ "__pypackages__",
77
+ "_build",
78
+ "buck-out",
79
+ "build",
80
+ "dist",
81
+ "node_modules",
82
+ "venv",
83
+ ]
84
+
85
+ # Same as Black.
86
+ line-length = 100
87
+
88
+ # Assume Python 3.12.
89
+ target-version = "py312"
90
+
91
+ [tool.ruff.lint]
92
+ # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
93
+ select = ["E", "F"]
94
+ ignore = ["E402", "E741", "E743", "E731"]
95
+
96
+ # Allow autofix for all enabled rules (when `--fix`) is provided.
97
+ fixable = ["A", "B", "C", "D", "E", "F"]
98
+ unfixable = []
99
+
100
+ # Allow unused variables when underscore-prefixed.
101
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
102
+
103
+
104
+ [tool.ruff.lint.mccabe]
105
+ # Unlike Flake8, default to a complexity level of 10.
106
+ max-complexity = 10
107
+
108
+ [tool.ruff.lint.per-file-ignores]
109
+ # Ignore "Line too long" (E501) for all files in the specific folder
110
+ "demo/*" = ["E501"]
111
+
112
+ [tool.bumpversion]
113
+ allow_dirty = false
114
+ commit = true
115
+ message = "Bump version: {current_version} → {new_version}"
116
+ tag = true
117
+ sign_tags = false
118
+ tag_name = "v{new_version}"
119
+ tag_message = "Bump version: {current_version} → {new_version}"
120
+ current_version = "0.1.0"
121
+
122
+
123
+ [[tool.bumpversion.files]]
124
+ filename = "pyproject.toml"
125
+ search = 'version = "{current_version}"'
126
+ replace = 'version = "{new_version}"'
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,56 @@
1
+ from .base import CardiacActivationModel
2
+ from .rdq18 import RDQ18
3
+ from .rdq20mf import RDQ20MF
4
+ from .lewalle2024 import Lewalle2024
5
+ from .land17 import Land2017
6
+ from . import utils
7
+ from .utils import calcium_trace, sl_trace
8
+
9
+ #: Maps a short model name to its class, so a model can be selected by name
10
+ #: (e.g. from a config file) instead of importing the class directly. Every
11
+ #: model here is a `CardiacActivationModel` and can be constructed uniformly
12
+ #: as `ModelClass(num_cells, Ta_max, params)`, so switching between them
13
+ #: requires no other code changes beyond the name/class used.
14
+ MODEL_REGISTRY: dict[str, type[CardiacActivationModel]] = {
15
+ "RDQ18": RDQ18,
16
+ "RDQ20MF": RDQ20MF,
17
+ "Lewalle2024": Lewalle2024,
18
+ "Land2017": Land2017,
19
+ }
20
+
21
+
22
+ def get_model(name: str) -> type[CardiacActivationModel]:
23
+ """
24
+ Look up a `CardiacActivationModel` subclass by name.
25
+
26
+ Parameters
27
+ ----------
28
+ name : str
29
+ One of the keys in `MODEL_REGISTRY` (e.g. "RDQ18", "RDQ20MF",
30
+ "Lewalle2024").
31
+
32
+ Returns
33
+ -------
34
+ type[CardiacActivationModel]
35
+ The model class, ready to be instantiated as
36
+ `get_model(name)(num_cells, Ta_max, params)`.
37
+ """
38
+ try:
39
+ return MODEL_REGISTRY[name]
40
+ except KeyError:
41
+ available = ", ".join(sorted(MODEL_REGISTRY))
42
+ raise KeyError(f"Unknown model {name!r}. Available models: {available}") from None
43
+
44
+
45
+ __all__ = [
46
+ "CardiacActivationModel",
47
+ "RDQ18",
48
+ "RDQ20MF",
49
+ "Lewalle2024",
50
+ "Land2017",
51
+ "MODEL_REGISTRY",
52
+ "get_model",
53
+ "utils",
54
+ "calcium_trace",
55
+ "sl_trace",
56
+ ]
@@ -0,0 +1,80 @@
1
+ from abc import ABC, abstractmethod
2
+ import numpy as np
3
+ import numpy.typing as npt
4
+
5
+
6
+ class CardiacActivationModel(ABC):
7
+ """
8
+ Abstract base class for all reduced-order cardiac activation models.
9
+ Designed to support vectorized execution across multiple cells or
10
+ integration points.
11
+ """
12
+
13
+ @abstractmethod
14
+ def __init__(self, num_cells: int, Ta_max: float, params: dict | None = None):
15
+ """
16
+ Initialize the model state and precompute necessary constants.
17
+
18
+ Parameters:
19
+ -----------
20
+ num_cells : int
21
+ The number of independent spatial units to simulate simultaneously.
22
+ Ta_max : float
23
+ The maximum active tension scaling factor.
24
+ params : dict, optional
25
+ Model-specific parameters to override defaults.
26
+ """
27
+ self.num_cells = num_cells
28
+ self.Ta_max = Ta_max
29
+
30
+ @classmethod
31
+ @abstractmethod
32
+ def default_parameters(cls) -> dict:
33
+ """
34
+ Return a dictionary of the default physiological parameters for the model.
35
+ """
36
+ pass
37
+
38
+ @abstractmethod
39
+ def advance_step(
40
+ self,
41
+ dt: float,
42
+ Ca_val: float | npt.NDArray[np.float64],
43
+ SL_vals: float | npt.NDArray[np.float64],
44
+ dSL_vals: float | npt.NDArray[np.float64] | None = None,
45
+ ) -> None:
46
+ """
47
+ Integrate the model's internal states forward by a single time step.
48
+
49
+ Parameters:
50
+ -----------
51
+ dt : float
52
+ The time step size in seconds.
53
+ Ca_val : float or np.ndarray
54
+ Intracellular calcium concentration.
55
+ SL_vals : float or np.ndarray
56
+ Current sarcomere length(s).
57
+ dSL_vals : float or np.ndarray, optional
58
+ Current sarcomere shortening velocity. Defaults to 0 if not provided.
59
+ """
60
+ pass
61
+
62
+ @abstractmethod
63
+ def get_active_tension(self) -> npt.NDArray[np.float64]:
64
+ """
65
+ Compute and return the macroscopic active tension (Ta) generated.
66
+
67
+ Returns:
68
+ --------
69
+ np.ndarray
70
+ The active tension for each cell/integration point (shape: `num_cells`).
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def reset(self) -> None:
76
+ """
77
+ Reset the model's internal state to its initial condition (as set by
78
+ `__init__`), without re-allocating precomputed constants.
79
+ """
80
+ pass