├── .gitignore
├── LICENSE
├── README.md
├── docs
├── _config.yml
├── api.md
├── fit_1to1.md
├── fit_competition.md
├── fit_homodimerbreaking.md
├── fit_homodimerformation.md
├── images
│ ├── Fig_1to1_fit.svg
│ ├── Fig_1to1_simulation.svg
│ ├── Fig_1to1_simulation_fraction_l.svg
│ ├── Fig_1to1_simulation_ymax.svg
│ ├── Fig_competition_fit.svg
│ ├── Fig_competition_simulation.svg
│ ├── Fig_custom_simulation.svg
│ ├── Fig_homodimerbreaking_fit.svg
│ ├── Fig_homodimerbreaking_simulation.svg
│ ├── Fig_homodimerformation_fit.svg
│ ├── Fig_homodimerformation_simulation.svg
│ ├── Fig_system_1to1.png
│ ├── Fig_system_competition.png
│ ├── Fig_system_custom.png
│ ├── Fig_system_homodimerbreaking.png
│ └── Fig_system_homodimerformation.png
├── index.md
├── simulate_1to1.md
├── simulate_competition.md
├── simulate_custom_system.md
├── simulate_homodimerbreaking.md
├── simulate_homodimerformation.md
└── tutorial.md
├── email-address-image.gif
├── example_1to1_fit.py
├── example_1to1_simulation.py
├── example_competition_fit.py
├── example_competition_simulation.py
├── example_custom_binding_system.py
├── example_custom_binding_system2.py
├── example_homodimer_breaking_fit.py
├── example_homodimer_breaking_simulation.py
├── example_homodimer_formation_fit.py
├── example_homodimer_formation_simulation.py
├── example_paper_figure1.py
├── example_paper_figure2.py
├── interrogate_system_solutions.py
├── pybindingcurve_logo.png
├── pyproject.toml
├── setup.py
├── src
└── pybindingcurve
│ ├── __init__.py
│ ├── pybindingcurve.py
│ └── systems
│ ├── __init__.py
│ ├── analytical_equations.py
│ ├── analytical_systems.py
│ ├── binding_system.py
│ ├── kinetic_systems.py
│ ├── kinetic_systems_1_to_1to5.py
│ ├── lagrange_binding_system_factory.py
│ ├── lagrange_systems.py
│ ├── minimizer_binding_system_factory.py
│ ├── minimizer_systems.py
│ └── systems.py
├── tests
├── conftest.py
├── test_competition.py
├── test_fitting.py
└── test_one_to_one.py
└── utils
├── example-DisplayHomoVsHetroDimerComparisonHeatmaps.py
├── example-GenerateHomoVsHetroDimerComparisonHeatmaps.py
└── example-HomoVsHetroDimerFormationBreaking.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Custom to pybindingcurve
2 | tmp_*
3 | tmp-*
4 | heatmap*.pkl
5 | .vscode
6 | system_results.csv
7 | testbed.py
8 | Figure?.*
9 | *.mp4
10 | # Byte-compiled / optimized / DLL files
11 | __pycache__/
12 | *.py[cod]
13 | *$py.class
14 | **/tmp.py
15 | **/tmp_example_output.py
16 | # C extensions
17 | *.so
18 | .testbed
19 |
20 |
21 | # Distribution / packaging
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 |
40 | # PyInstaller
41 | # Usually these files are written by a python script from a template
42 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
43 | *.manifest
44 | *.spec
45 |
46 | # Installer logs
47 | pip-log.txt
48 | pip-delete-this-directory.txt
49 |
50 | # Unit test / coverage reports
51 | htmlcov/
52 | .tox/
53 | .coverage
54 | .coverage.*
55 | .cache
56 | nosetests.xml
57 | coverage.xml
58 | *.cover
59 | .hypothesis/
60 | .pytest_cache/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # celery beat schedule file
91 | celerybeat-schedule
92 |
93 | # SageMath parsed files
94 | *.sage.py
95 |
96 | # Environments
97 | .env
98 | .venv
99 | env/
100 | venv/
101 | ENV/
102 | env.bak/
103 | venv.bak/
104 |
105 | # Spyder project settings
106 | .spyderproject
107 | .spyproject
108 |
109 | # Rope project settings
110 | .ropeproject
111 |
112 | # mkdocs documentation
113 | /site
114 |
115 | # mypy
116 | .mypy_cache/
117 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Steven Shave
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyBindingCurve
2 |
3 | *Shave, Steven, et al. "PyBindingCurve, simulation, and curve fitting to complex binding systems at equilibrium." Journal of Chemical Information and Modeling (2021).* https://doi.org/10.1021/acs.jcim.1c00216
4 |
5 | PyBindingCurve is a Python package for simulation, plotting and fitting of experimental parameters to protein-ligand binding systems at equilibrium. In simple terms, the most basic functionality allows simulation of a two species binding to each other as a function of their concentrations and the dissociation constant (KD) between the two species. A number of systems are built in and can be solved using direct analytical, kinetic, or Langrange multiplier based techniques. User-defined custom systems can also be specified using a simple syntax.
6 |
7 | Try without installing on Google colab! https://colab.research.google.com/drive/1upxm56mGYWo8jvTTJjZLOEq6DT0lRy8d
8 |
9 |
10 | 
11 |
12 | # Installation
13 | PyBindingCurve may be installed from source present in the GitHub repository https://github.com/stevenshave/pybindingcurve via git pull, or from the Python Package Index (https://pypi.org/project/pybindingcurve/) using the command :
14 | > pip install pybindingcurve
15 |
16 | # Requirements
17 | PyBindingCurve requires Python 3.9 or later. The following packages are also required
18 | - numpy>=1.26
19 | - matplotlib>=3.8
20 | - lmfit>=1.2.2
21 | - mpmath>=1.3.0
22 | - autograd>=1.6.2
23 |
24 | # License
25 | [MIT License](https://github.com/stevenshave/pybindingcurve/blob/master/LICENSE)
26 |
27 |
28 |
29 | # Usage
30 | A tutorial and API documentation can be found [here](https://stevenshave.github.io/pybindingcurve/)
31 |
32 | A quickstart example for simulation of protein-ligand binding is as follows:
33 |
34 | ```
35 | import numpy as np
36 | import pybindingcurve as pbc
37 | my_system = pbc.BindingCurve("1:1")
38 | system_parameters = {"p": np.linspace(0, 20), "l": 10, "kdpl": 1}
39 | my_system.add_curve(system_parameters)
40 | my_system.show_plot()
41 | ```
42 | Tests written using the pytest framework may be run with 'pytest' (ensure pytest is installed in your python environment, or pip install it)
43 |
44 | # Authors
45 | PyBindingCurve was written by Steven Shave
46 | 
47 |
48 |
49 | Please get in contact for custom solutions, integration to existing workflows and training.
50 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-tactile
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # PyBindingCurve API reference
2 |
3 | Documentation and API
4 | Full PyBindingCurve source code can be found here: https://github.com/stevenshave/pybindingcurve
5 |
6 | - [Overview](#Overview)
7 | - [pbc.BindingCurve](#pbc.BindingCurve)
8 | - [Initialisation](###Initialisation)
9 | - [add_curve](###add_curve)
10 | - [query](###query)
11 | - [fit](###fit)
12 | - [add_scatter](###add_scatter)
13 | - [show_plot](###show_plot)
14 | - [pbc.systems and shortcut strings](##pbc.systems)
15 | - [pbc.BindingSystem](##pbc.BindingSystem)
16 | - [pbc.Readout](##pbc.Readout)
17 |
18 |
19 | ## Overview
20 | Conventionally, the standard import utilised in a run of PyBindingCurve (PBC) are defined as follows:
21 | import pybindingcurve as pbc
22 | import numpy as np
23 | PyBindingCurve is imported with the short name ‘pbc’, and then NumPy as ‘np’ to enable easy specification of ranged system parameters, evenly spaced across intervals mimicking titrations.
24 | Next, we initialise a PBC BindingCurve object. Upon initialisation, either a string or BindingSystem object is used to define the system to be simulated. Simple strings such as “1:1”, “competition”, “homodimer breaking” can be used as an easy way to define the type of binding system that should be mathematically modelled. See the list of available system shortcut strings bellow in the ‘pbc.systems and shortcut strings’ section bellow. Custom objects may also be created of type pybindingcurve.BindingSystem, of which there exist a large choice within pybindingcurve.systems, or the user may create a custom BindingSystem to initialise PBC objects:
25 | my_system=pbc.BindingCurve(“1:1”)
26 | There are three main modes of operation within PyBindingCurve; 1) Visualisation of protein-ligand behaviour within a titration, simulating a range of conditions. 2) Simulation of a single system state with discrete parameters. 3) Fitting of experimental data. A description of these follows:
27 | 1. Simulation with visualisation: pass a dictionary containing system parameters to the add_curve function of the BindingCurve object (my_system). Required system parameters depend on the system being modelled. In the case of 1:1 binding, we require p (protein concentration), l (ligand concentration), kdpl (dissociation constant between p and l), and optionally, a ymax and/or ymin variable if dealing with simulation of a signal. add_curve expects one changing parameter, which will be the x-axis. By default, complex concentration will be the readout of a system, but that can be changed by passing different readout options to add_curve. See the ‘pbc.Readout’ section bellow for further information. With one curve added, we can add more curves, or simply display the plot by calling the show_plot function.
28 |
29 | ```python
30 | system_parameters={‘p’:np.linspace(0,20), ‘l’:20, ‘kdpl’:10)
31 | my_system.add_curve(system_parameters)
32 | my_system.show_plot()
33 | ```
34 |
35 | 2. Single point simulation: if you require not a simulation with a curve, but a single point with set concentrations and KDs, then query may be called with a dictionary of system parameters and data returned. Additionally, if the dictionary contains a NumPy array, representing a titration (for example, the system parameters above), then a NumPy array of the readout is returned instead of a single value:
36 |
37 | ```python
38 | complex_conc = my_system.query({‘p’:10, ‘l’:20, ‘kdpl’:10})
39 | ```
40 |
41 | 3. Fit experimental data to a system to obtain experimental parameters, such as KD. A common situation is determining KD from measurements obtained from experimental data. We can perform this as follows, with x- and y-coordinates, we add the experimental points to the plot, define system parameters that we do know (protein concentrations, and the amount of ligand), and then call fit on the system passing in parameters to fit and an initial guess (1), along with the known parameters. We then iterate and print the fitted parameters.
42 |
43 | ```python
44 | xcoords = np.array([0.0, 20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0])
45 | ycoords = np.array([0.544, 4.832, 6.367, 7.093, 7.987, 9.005, 9.079, 8.906, 9.010, 10.046, 9.225])
46 | my_system.add_scatter(xcoords, ycoords)
47 | system_parameters = {"p": xcoords, "l": 10}
48 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpl": 1}, ycoords)
49 | for k, v in fit_accuracy.items():
50 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
51 | ```
52 |
53 | ## pbc.BindingCurve
54 | The BindingCurve object allows the user to work with a specific system, supplying tools for simulation and visualisation (plotting), querying of single point values, and fitting of experimental parameters to observation data.
55 | ### Initialisation
56 | When initialising this main class of PBC, we may supply either a pbc.BindingSystem, a human readable shortcut string such as “1:1”, “competition”, “homodimer formation”, etc, or a system definition string. For a full list of systems, shortcuts, and custom systems definition strings, please refer to the ‘pbc.systems and shortcut strings’ section.
57 |
58 | Initialisation of a BindingCurve object takes the following arguments:
59 |
60 | """
61 | BindingCurve class, used to simulate systems
62 |
63 | BindingCurve objects are governed by their underlying system, defining the
64 | (usually) protein-ligand binding system being represented. It also
65 | provides the main interface for simulation, visualisation, querying and
66 | the fitting of system parameters.
67 |
68 | Parameters
69 | ----------
70 | binding_system : BindingSystem or str
71 | Define the binding system which will govern this BindingCurve object.
72 | Can either be a BindingSystem object, a shortcut string describing a
73 | system (such as '1:1' or 'competition', etc), or a custom binding
74 | system definition string.
75 |
76 | """
77 |
78 |
79 | Once intitialised with a pbc.BindingSystem, we may perform the following utilising its member functions.
80 | ### add_curve
81 | The add curve function is the main way of simulating a binding curve with PBC. Once a BindingCurve object is initialised,
82 |
83 | """
84 | Add a curve to the plot
85 |
86 | Add a curve as specified by the system parameters to the
87 | pbc.BindingSystem's internal plot using the underlying binding system
88 | specified on intitialisation.
89 |
90 | Parameters
91 | ----------
92 | parameters : dict
93 | Parameters defining the system to be simulated
94 | name : str or None, optional
95 | Name of curve to appear in plot legends
96 | readout : Readout.function, optional
97 | Change the system readout to one described by a custom readout
98 | function. Predefined standard readouts can be found in the static
99 | pbc.Readout class.
100 | """
101 | ### query
102 | When simulation with visualisation (plotting) is not required, we can use the query function to interrogate a system, returning either singular values, or arrays of values if one of the input parameters is an array or list.
103 |
104 | """
105 | Query a binding system
106 |
107 | Get the readout from from a set of system parameters
108 |
109 | Parameters
110 | ----------
111 | parameters : dict
112 | System parameters defining the system being queried. Will usually
113 | contain protein, ligand etc concentrations, and KDs
114 | readout : func or None
115 | Change the readout of the system, can be None for unmodified
116 | (usually complex concentration), a static member function from
117 | the pbc.Readout class, or a custom written function following the
118 | the same defininition as those in pbc.Readout.
119 |
120 | Returns
121 | -------
122 | Single floating point, or array-like
123 | Response/signal of the system
124 | """
125 | ### fit
126 | With a system defined, we may fit experimental data to the system.
127 |
128 | """
129 | Fit the parameters of a system to a set of data points
130 |
131 | Fit the system to a set of (usually) experimental datapoints.
132 | The fitted parameters are stored in the system_parameters dict
133 | which may be accessed after running this function. It is
134 | possible to fit multiple parameters at once and define bounds
135 | for the parameters. The function returns a dictionary of the
136 | accuracy of fitted parameters, which may be captured, or not.
137 |
138 | Parameters:
139 | system_parameters : dict
140 | Dictionary containing system parameters, will be used as arguments
141 | to the systems equations.
142 | to_fit : dict
143 | Dictionary containing system parameters to fit.
144 | xcoords : np.array
145 | X coordinates of data the system parameters should be fit to
146 | ycoords : np.array
147 | Y coordinates of data the system parameters should be fit to
148 | bounds : dict
149 | Dictionary of tuples, indexed by system parameters denoting the
150 | lower and upper bounds of a system parameter being fit, optional,
151 | default = None
152 |
153 | Returns
154 | -------
155 | tuple (dict, dict)
156 | Tuple containing a dictionary of best fit systems parameters,
157 | then a dictionary containing the accuracy for fitted variables.
158 | """
159 | ### add_scatter
160 | Experimental data can be added plots with the add_scatter command, taking a simple list of x and y coordinates
161 |
162 | """
163 | Add scatterpoints to a plot, useful to represent real measurement data
164 |
165 | X and Y coordinates may be added to the internal plot, useful when
166 | fitting to experimental data, and wanting to plot the true experimental
167 | values alongside a curve generated with fitted parameters.
168 |
169 | Parameters
170 | ----------
171 | xcoords : list or array-like
172 | x-coordinates
173 | ycoords : list or array-like
174 | y-coordinates
175 |
176 | """
177 |
178 | ### show_plot
179 | With curves, scatterpoints and fits applied, we may display the plot.
180 |
181 | """
182 | Show the PyBindingCurve plot
183 |
184 | Function to display the internal state of the pbc BindingCurve objects
185 | plot.
186 |
187 | Parameters
188 | ----------
189 | title : str
190 | The title of the plot (default = "System simulation")
191 | xlabel: str
192 | X-axis label (default = None)
193 | ylabel : str
194 | Y-axis label (default = None, causing label to be "[Complex]")
195 | min_x : float
196 | X-axis minimum (default = None)
197 | max_x : float
198 | X-axis maximum (default = None)
199 | min_y : float
200 | Y-axis minimum (default = None)
201 | max_y : float
202 | Y-axis maximum (default = None)
203 | log_x_axis : bool
204 | Log scale on X-axis (default = False)
205 | log_y_axis : bool
206 | Log scale on Y-axis (default = False)
207 | ma_style : bool
208 | Apply MA styling, making plots appear like GraFit plots
209 | png_filename : str
210 | File name/location where png will be written
211 | svg_filename : str
212 | File name/location where svg will be written
213 |
214 | """
215 |
216 |
217 | ## pbc.systems
218 |
219 | pbc.systems contains all default systems supplied with PBC, and exports them to the PBC namespace. Systems may be passed as arguments to pbc.BindingCurve objects upon initialization to define the underlying system governing simulation, queries, and fitting. Additionally, the following shortcut strings may be used as shortcuts, all spaces are removed from the input string, and so are represented without whitespace bellow:
220 |
221 | |Shortcut string list|pbc.systems equivalent|
222 | |---|---|
223 | |simple, 1:1, 1:1analytical|System_analytical_one_to_one__pl|
224 | |1:1min, 1:1minimizer, 1:1minimiser|System_minimizer_one_to_one__pl|
225 | |simplelagrange, 1:1lagrange|System_lagrange_one_to_one__pl|
226 | |simplekinetic, 1:1kinetic| System_kinetic_one_to_one__pl|
227 | |homodimerformation| System_analytical_homodimerformation__pp|
228 | |homodimerformationmin, homodimerformationminimiser, homodimerformationminimizer, homodimermin, homodimerminimiser, homodimerminimizer| System_minimizer_homodimerformation__pp|
229 | |homodimerformationlagrange|System_lagrange_homodimerformation__pp|
230 | |homodimerformationkinetic|System_kinetic_homodimerformation__pp|
231 | |competition, 1:1:1|System_analytical_competition__pl|
232 | |competitionmin, competitionminimiser, competitionminimizer, 1:1:1min, 1:1:1minimiser, 1:1:1minimizer|System_minimizer_competition__pl|
233 | |competitionlagrange|System_lagrange_competition__pl|
234 | |homodimerbreaking, homodimerbreakingmin, homodimerbreakingminimiser, homodimerbreakingminimizer|System_minimizer_homodimerbreaking__pp|
235 | |homodimerbreakinglagrange|System_lagrange_homodimerbreaking__pp|
236 | |homodimerbreakingkinetic|System_kinetic_homodimerbreaking__pp|
237 | |homodimerbreakinganalytical|System_analytical_homodimerbreaking__pp|
238 | |1:2, 1:2min, 1:2minimizer, 1:2minimiser| System_minimizer_1_to_2__pl12|
239 | |1:2lagrange| System_lagrange_1_to_2__pl12|
240 | |1:3, 1:3min, 1:3minimizer, 1:3minimiser|System_minimizer_1_to_3__pl123|
241 | |1:3lagrange| System_lagrange_1_to_3__pl123|
242 | |1:4, 1:4lagrange| System_lagrange_1_to_4__pl1234|
243 | |1:5, 1:5lagrange| System_lagrange_1_to_5__pl12345|
244 |
245 | Custom systems can be passed allowing the use of custom binding systems derived from a simple syntax. This is in the form of a string with reactions separated either on newlines, commas, or a combination of the two. Reactions take the form:
246 |
247 | - r1+r2<->p
248 |
249 | Denoting reactant1 + reactant2 form the product. PBC will generate and solve custom defined constrained systems. Readouts are signified by inclusion of a star (*) on a species. If no star is found, then the first seen product is used. Some system examples follow:
250 |
251 | - "P+L<->PL" - standard protein-ligand binding
252 | - "P+L<->PL, P+I<->PI" - competition binding
253 | - "P+P<->PP" - dimer formation
254 | - "monomer+monomer<->dimer" - dimer formation
255 | - "P+L<->PL1, P+L<->PL2, PL1+L<->PL1L2, PL2+L<->PL1L2" - 1:2 site binding
256 |
257 | KDs passed to custom systems use underscores to separate species and products. P+L<->PL would require the KD passed as kd_p_l_pl. Running with incomplete system parameters will prompt for the correct ones.
258 |
259 |
260 | ## pbc.BindingSystem
261 | Custom binding systems may be defined through inheritance from the base class pbc.BindingSystem. This provides basic functionality through a standard interface to PBC, allowing simulation, querying and fitting. It expects the child class to provide a constructor which passes a function for querying the system and a query method. An example pbc.BindingSystem for 1:1 binding solved analytically is defined as follows:
262 |
263 | ```
264 | class System_analytical_one_to_one_pl(BindingSystem):
265 | def __init__(self):
266 | super().__init__(
267 | analyticalsystems.system01_one_to_one__p_l_kd__pl, analytical=True
268 | )
269 | self.default_readout = "pl"
270 |
271 | def query(self, parameters: dict):
272 | if self._are_ymin_ymax_present(parameters):
273 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
274 | parameters
275 | )
276 | value = super().query(parameters_no_min_max)
277 | with np.errstate(divide="ignore", invalid="ignore"):
278 | return (
279 | parameters["ymin"]
280 | + ((parameters["ymax"] - parameters["ymin"]) * value)
281 | / parameters["l"]
282 | )
283 | else:
284 | return super().query(parameters)
285 | ```
286 | Here, we see the parent class constructor called upon initialisation of the object with two arguments, the first is a python function which calculates the complex concentration present in a 1:1 binding system, which itself takes the appropriate parameters to calculate this. In addition, a flag is set to define when the solution is solved analytically. The query method examines the content of the system and deals with the presence of ymin and ymax to denote a signal is being simulated. Query should ultimately end up calling query on the parent class, which has been set to return the result of the previously assigned function in the constructor.
287 |
288 | ## pbc.Readout
289 | The pbc.Readout class contains three static methods, not requiring object initialisation for use. These methods all take in a system parameters dictionary describing the system, and the y_values resulting from system query calls (either through simulation of querying for singular values). These readout functions offer a convenient way to transform results. For example, the readout function to transform complex concentration into fraction ligand bound is defined as follows:
290 | ```
291 | def fraction_l(system_parameters: dict, y):
292 | """ Readout as fraction ligand bound """
293 | return "Fraction l bound", y / system_parameters["l"]
294 | ```
295 | This returns a tuple, with the first value being used in labelling of the plot y-axis, and the second the y-values to be plotted; in this case, the original y values divided by the overall starting ligand concentration. Similar functions can be defined and used interchangeably with those found in pbc.Readout.
296 |
--------------------------------------------------------------------------------
/docs/fit_1to1.md:
--------------------------------------------------------------------------------
1 | # Fitting of data to 1:1 binding
2 | 
3 |
4 | [Return to tutorials](tutorial.md)
5 |
6 | Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_1to1_fit.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_1to1_fit.py)
7 |
8 | A titration of protein from nothing to 200 µM into a cuvette containing 10 µM ligand produces the following signal from the instrument:
9 |
10 | | [P] µM | 0 | 20 |40|60|80|100|120|140|160|180|200|
11 | |---|---|---|---|---|---|---|---|---|---|---|---
12 | | Signal | 54.4|483.2|636.7|709.3|798.7|900.5|907.9|890.6|901.0|1004.6|922.5|
13 |
14 | We may easily fit this data using PBC and obtain two values… the KD of the complex, as well as the maximal signal (ymax) achievable with the system.
15 | We first import PBC and Numpy:
16 | ```python
17 | import numpy as np
18 | import pybindingcurve as pbc
19 | ```
20 | Next we create NumPy arrays to hold our protein concentration and signal:
21 |
22 | ```python
23 | xcoords = np.array([0.0, 20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0])
24 | ycoords = np.array([54.4, 483.2, 636.7, 709.3, 798.7, 900.5, 907.9, 890.6, 901.0, 1004.6, 922.5])
25 | ```
26 | We now define a PBC BindingCurve object governed by the 1:1 binding system, and add the experimental data to PBC’s internal plot using the add_scatter function of the returned BindingCurve object:
27 | ```python
28 | mySystem = pbc.BindingCurve("1:1")
29 | mySystem.add_scatter(xcoords, ycoords)
30 | ```
31 | When simulating a curve, we define a system with all parameters required to fully describe the simulation. In this case, we know four things; the amount of protein in the system (xcoords), the concentration of ligand present, the minimal signal, and finally the response. We supply the amount of protein and ligand, along with the known minimum signal for no protein being present (calculated by calling np.min on ycoords), and not yet the response to define the system which will be supplied as an argument to the fitting function in the next code segment:
32 |
33 | ```python
34 | system_parameters = {"p": xcoords, "l": 10, “ymin”:np.min(ycoords)}
35 | ```
36 |
37 | We then perform the fit, capturing two pieces of data; the fitted system (a dictionary of system parameters best describing the system) and the fit accuracy data. The call to PBC.BindingCurve.fit takes the known system parameters, followed by the unknown system parameters, and finally the signal data which we are fitting to. Unknown system parameters are passed in a dictionary, much like the system parameters, but their assigned value is only used as a starting point guess for the fitting routines and can normally be set to any value. A reasonable guess at the true value, however, is good practice. Inclusion of either ymin or ymax as either a known, or unknown system parameters allows PBC to infer that we are fitting to a signal, not an absolute, known complex concentration:
38 |
39 | ```python
40 | fitted_system, fit_accuracy = mySystem.fit(system_parameters, {"kdpl": 0, “ymax”:1000}, ycoords)
41 | ```
42 |
43 | We may now print out the fitted paramers, along with the accuracy of the parameters. Internally, PBC utilises the lmfit package to return fitted parameters along with a true fit accuracy, specifying a range within which we are 95% certain that the true value is within:
44 |
45 | ```python
46 | for k, v in fit_accuracy.items():
47 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
48 | ````
49 |
50 | Running the above code results in the following output:
51 |
52 | > Fit: kdpl=24.720148154934403 +/- 3.800620728975203
53 |
54 | > Fit: ymax=1072.308288861583 +/- 34.03937019626566
55 |
56 | Indicating that the system’s KD is 24.72 +/- 3.8 µM.
57 |
58 | To visualise how well this fit describes the experimental data, we can use the returned system parameters to plot a curve over the scatter data already added to the pbc.BindingCurve object’s internal plot. However, the returned system object currently looks like this:
59 | > {'p': array([ 0., 20., 40., 60., 80., 100., 120., 140., 160., 180., 200.]), 'l': 10, 'ymin': 54.4, 'kdpl': 24.720148154934403, 'ymax': 1072.308288861583}
60 |
61 | Simulating and plotting such a system would produce a plot with only 11 points along the x-axis and would not look correct. We therefore increase the number of points present for protein concentration with the following command:
62 |
63 | ```python
64 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
65 | ```
66 |
67 | We may then add the curve to the plot and visualise the result.
68 |
69 | ```python
70 | mySystem.add_curve(fitted_system)
71 | mySystem.show_plot()
72 | ```
73 |
74 | Resulting in the following:
75 | 
76 |
77 |
78 | [Return to tutorials](tutorial.md)
79 |
--------------------------------------------------------------------------------
/docs/fit_competition.md:
--------------------------------------------------------------------------------
1 | # Fitting to 1:1:1 competition
2 | 
3 | [Return to tutorials](tutorial.md)
4 |
5 |
6 | Using experimental competition data, we may obtain system parameters, such as inhibitor KD. Example code is available here:
7 | [https://github.com/stevenshave/pybindingcurve/blob/master/example_competition_fit.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_competition_fit.py)
8 |
9 | Perform the standard imports:
10 | ```python
11 | import numpy as np
12 | import pybindingcurve as pbc
13 | ```
14 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow.
15 |
16 | Using the following experimental data:
17 | |[P] µM | 0|4.2|8.4|16.8|21.1|31.6|35.8|40.0|
18 | |---|---|---|---|---|---|---|---|---|
19 | Signal|150|330|1050|3080|4300|6330|6490|6960|
20 |
21 | Define x and y coordinates from experimental data:
22 | ```python
23 | xcoords = np.array([0.0, 4.2, 8.4, 16.8, 21.1, 31.6, 35.8, 40.0])
24 | ycoords = np.array([150, 330, 1050, 3080, 4300, 6330, 6490, 6960])
25 | ```
26 |
27 | Construct the PyBindingCurve object, operating on a 1:1:1 (compeittion) system and add experimental data to the plot:
28 |
29 | ```python
30 | mySystem = pbc.BindingCurve("1:1:1")
31 | mySystem.add_scatter(xcoords, ycoords)
32 | ```
33 |
34 | Known system parameters, kdpl will be added to this by fitting:
35 |
36 | ```python
37 | system_parameters = {"p": xcoords, "l": 10, "i": 10, "kdpl": 10}
38 | ```
39 |
40 | Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along with an initial guess, pass the ycoords, and what the readout (ycoords) is:
41 |
42 | ```python
43 | fitted_system, fit_accuracy = mySystem.fit(system_parameters, {"kdpi": 0}, ycoords)
44 | ```
45 | Print out the fitted parameters:
46 |
47 | ```python
48 | for k, v in fit_accuracy.items():
49 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
50 | ```
51 |
52 | Assign more points to 'p' to make a smooth plot:
53 |
54 | ```python
55 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
56 | ```
57 |
58 | Add a new curve, simulated using fitted parameters to our BindingCurve object and show the plot:
59 |
60 | ```python
61 | mySystem.add_curve(fitted_system)
62 | mySystem.show_plot()
63 | ```
64 |
65 | Which results in the following output and plot:
66 | > Fit: kdpi=0.44680894202996824 +/- 0.10384753604598472
67 | >
68 | > Fit: ymax=9920.875421523158 +/- 98.92963212643627
69 |
70 | 
71 |
72 |
73 | [Return to tutorials](tutorial.md)
74 |
--------------------------------------------------------------------------------
/docs/fit_homodimerbreaking.md:
--------------------------------------------------------------------------------
1 | # Fitting of data to homodimer formation
2 | 
3 |
4 | [Return to tutorials](tutorial.md)
5 |
6 | Using experimental competition data, we may obtain system parameters, such as dimerisation KD. Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_fit.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_fit.py)
7 | Perform the standard imports:
8 |
9 | ```python
10 | import numpy as np
11 | import pybindingcurve as pbc
12 | ```
13 |
14 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow:
15 |
16 | Define experimental data:
17 |
18 | ```python
19 | xcoords = np.array([0.0, 16.7, 33.3, 50.0, 66.7, 83.3, 100.0])
20 | ycoords = np.array([0.0, 0.004, 0.021, 0.094, 0.312, 1.188, 3.854])
21 | ```
22 |
23 | Construct the PyBindingCurve object, operating on a homodimer breaking system and add experimental data to the plot:
24 |
25 | ```python
26 | my_system = pbc.BindingCurve("homodimerbreaking")
27 | my_system.add_scatter(xcoords, ycoords)
28 | ```
29 |
30 | Known system parameters, kdpl will be added to this by fitting:
31 |
32 | ```python
33 | system_parameters = {"p": xcoords, "i": 100, "kdpp": 10}
34 | ```
35 |
36 | Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along with an initial guess, pass the ycoords, and what the readout (ycoords) is:
37 |
38 | ```python
39 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpi": 0}, ycoords)
40 | ```
41 |
42 | Print out the fitted parameters:
43 | ```python
44 | for k, v in fit_accuracy.items():
45 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
46 | ```
47 |
48 | Assign more points to 'p' to make a smooth plot:
49 |
50 | ```python
51 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
52 | ```
53 |
54 | Add a new curve, simulated using fitted parameters to our BindingCurve object and display the plot:
55 |
56 | ```python
57 | my_system.add_curve(fitted_system)
58 | my_system.show_plot()
59 | ```
60 |
61 | Resulting in:
62 | > Fit: kdpi=1.0024780308947485 +/- 0.001698935583536732
63 |
64 | 
65 |
66 |
67 | [Return to tutorials](tutorial.md)
68 |
--------------------------------------------------------------------------------
/docs/fit_homodimerformation.md:
--------------------------------------------------------------------------------
1 | # Fitting of data to homodimer formation
2 | 
3 |
4 | [Return to tutorials](tutorial.md)
5 |
6 | Using experimental competition data, we may obtain system parameters, such as dimerisation KD. Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_fit.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_fit.py)
7 | Perform the standard imports:
8 |
9 | ```python
10 | import numpy as np
11 | import pybindingcurve as pbc
12 | ```
13 |
14 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow.
15 |
16 | We define the known experimental data bellow:
17 |
18 | ```python
19 | xcoords = np.array([0.0,2,4,6,8,10])
20 | ycoords = np.array([0., 0.22, 0.71, 1.24,1.88,2.48])
21 | ```
22 |
23 | Construct the PyBindingCurve object, operating on a homodimer formation system and add experimental data to the plot:
24 |
25 | ```python
26 | mySystem = pbc.BindingCurve("homodimer formation")
27 | mySystem.add_scatter(xcoords, ycoords)
28 | ```
29 |
30 | Known system parameters, kdpp will be added to this by fitting:
31 |
32 | ```python
33 | system_parameters = {"p": xcoords}
34 | ```
35 |
36 | Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along with an initial guess, pass the ycoords, and what the readout (ycoords) is:
37 |
38 | ```python
39 | fitted_system, fit_accuracy = mySystem.fit(system_parameters, {"kdpp": 0}, ycoords)
40 | ```
41 |
42 | Print out the fitted parameters:
43 |
44 | ```python
45 | for k, v in fit_accuracy.items():
46 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
47 | ```
48 |
49 | Producing:
50 | > Fit: kdpp=9.939776196471206 +/- 0.15729785759220752
51 |
52 |
53 | Assign more points to 'p' to make a smooth plot:
54 |
55 | ```python
56 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
57 | ```
58 |
59 | Add a new curve, simulated using fitted parameters to our BindingCurve object and show the plot:
60 |
61 | ```python
62 | mySystem.add_curve(fitted_system)
63 | mySystem.show_plot()
64 | ```
65 |
66 | Producing:
67 | 
68 |
69 |
70 | [Return to tutorials](tutorial.md)
71 |
--------------------------------------------------------------------------------
/docs/images/Fig_system_1to1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/docs/images/Fig_system_1to1.png
--------------------------------------------------------------------------------
/docs/images/Fig_system_competition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/docs/images/Fig_system_competition.png
--------------------------------------------------------------------------------
/docs/images/Fig_system_custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/docs/images/Fig_system_custom.png
--------------------------------------------------------------------------------
/docs/images/Fig_system_homodimerbreaking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/docs/images/Fig_system_homodimerbreaking.png
--------------------------------------------------------------------------------
/docs/images/Fig_system_homodimerformation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/docs/images/Fig_system_homodimerformation.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # PyBindingCurve
2 | - [Tutorial](tutorial.md)
3 | - [API documentation](api.md)
4 | - [PyBindingCurve source](https://github.com/stevenshave/pybindingcurve)
5 |
6 | PyBindingCurve is a Python package for simulation, plotting and fitting of experimental parameters to protein-ligand binding systems at equilibrium. In simple terms, the most basic functionality allows simulation of a two species binding to each other as a function of their concentrations and the dissociation constant (KD) between the two species.
7 |
8 | 
9 |
10 | # Installation
11 | PyBindingCurve may be installed from source present in the GitHub repository https://github.com/stevenshave/pybindingcurve via git pull, or from the Python Package Index (https://pypi.org/project/pybindingcurve/) using the command :
12 | > pip install pybindingcurve
13 |
14 | # Requirements
15 | PyBindingCurve was developed using python 3.7.1 but should work with any Python version 3.6 or greater. The following packages are also required
16 | - Matplotlib (2.x)
17 | - Numpy (1.15.x)
18 | - lm_fit (1.0.0)
19 | - mpmath (1.1.0)
20 | - autograd (1.3)
21 |
22 | # Licence
23 | [MIT License](https://github.com/stevenshave/pybindingcurve/blob/master/LICENSE)
24 |
25 |
26 |
27 | # Tutorial
28 | A tutorial and API documentation can be found [here](https://stevenshave.github.io/pybindingcurve/tutorial)
29 |
30 | # API documentation
31 | API docmentation can be found [here](https://stevenshave.github.io/pybindingcurve/api)
32 |
--------------------------------------------------------------------------------
/docs/simulate_1to1.md:
--------------------------------------------------------------------------------
1 | # Simulation of 1:1 binding and exploration of all options
2 | 
3 | [Return to tutorials](tutorial.md)
4 |
5 | Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_1to1_simulation.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_1to1_simulation.py)
6 |
7 | A 1:1 binding system typically consists of a protein and a ligand. However, this 1:1 system is suitable for simulation of any two different species forming a complex. This system can therefore be used to simulate hetero-dimer formation, where two different proteins form a complex. In this simple example, we will imagine wanting to know the concentration of a complex formed. We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations and KDs bellow.
8 |
9 | First, we may want to produce a plot of protein vs complex concentration for a fixed amount of ligand, simulating the titration of protein into a cuvette for example. We must import PyBindingCurve and NumPy, and then make a new BindingCurve object which takes as an argument the type of system that should be represented. In this case “1:1” will define the correct system. For a list of all systems, please see “pbc.systems and shortcut strings” in the PyBindingCurve documentation.
10 | ```python
11 | import numpy as np
12 | import pybindingcurve as pbc
13 | my_system = pbc.BindingCurve("1:1")
14 | ```
15 | We then define the system parameters in a python dictionary, defining p (protein concentration) as a linear NumPy sequence from 0 to 20 µM, l (ligand concentration) as 10 µM, and the protein-ligand KD to be 1 µM.
16 | ```python
17 | system_parameters = {"p": np.linspace(0, 20), "l": 10, "kdpl": 1}
18 | ```
19 | We can now add the curve to the plot. If we want multiple simulations on the same plot, then it is good to give the curve a name with the optional name parameter.
20 | ```python
21 | my_system.add_curve(system_parameters, name= “Curve 1”)
22 | ```
23 | Now we add another, higher affinity curve to the plot, defining a similar system with a lower KD and add it to the curve.
24 | ```python
25 | system_parameters_higher_affinity = {"p": np.linspace(0, 20), "l": 10, "kdpl": 0.5}
26 | my_system.add_curve(system_parameters_higher_affinity, name “Curve 2”)
27 | ```
28 |
29 | Finally show the plot. Optionally, title, xlabel and ylabel variables may also be passed to title the plot and axes.
30 | ```python
31 | my_system.show_plot()
32 | ```
33 | This produces the following plot:
34 | 
35 |
36 | To obtain exact single points from the plot, we may call the query function of my_system:
37 | ```python
38 | print(my_system.query({“p”:5, “l”:10, “kdpl”:1})
39 | ```
40 | If a list or NumPy array is included as a system parameter, then a NumPy array of results is returned.
41 |
42 | We may want to simulate a system in terms of a signal, not the concentration of complex. In this case, we may pass additional parameters, setting the ymax and/or ymin variables in the system parameters. Inclusion of these will scale the signal present between these values. This is very important if a detector is used with a maximum or minimum sensitivity and you wish to simulate response. Changing our system parameters to include a maximal response of 2000 units will result in subtle scale and axes changes:
43 | ```python
44 | system_parameters = {"p": np.linspace(0, 20), "l": 10, "kdpl": 1, 'ymax':2000}
45 | system_parameters_higher_affinity = {"p": np.linspace(0, 20), "l": 10, "kdpl": 0.5, 'ymax':2000}
46 | my_system.add_curve(system_parameters, name= “Curve 1”)
47 | my_system.add_curve(system_parameters_higher_affinity, name= “Curve 2”)
48 | my_system.show_plot()
49 | ```
50 | Resulting in the following:
51 | 
52 |
53 | Additionally, we may transform the readout by passing a range of pbc.Readout objects to add_curve, or query. To display curves as a fraction total ligand bound, we would pass pbc.Readout.fracion_l. For more information on readouts, please see pbc.Readout in the PyBindingCurve documentation. Supplying a readout overrides the automatic signal readout selection when a ymin or ymax parameter has been found in a system:
54 | ```python
55 | system_parameters = {"p": np.linspace(0, 20), "l": 10, "kdpl": 1, 'ymax':2000}
56 | system_parameters_higher_affinity = {"p": np.linspace(0, 20), "l": 10, "kdpl": 0.5, 'ymax':2000}
57 | my_system.add_curve(system_parameters, readout=pbc.Readout.fraction_l)
58 | my_system.add_curve(system_parameters_higher_affinity, readout=pbc.Readout.fraction_l)
59 | my_system.show_plot()
60 | ```
61 | Results in the following:
62 |
63 | 
64 |
65 | [Return to tutorials](tutorial.md)
--------------------------------------------------------------------------------
/docs/simulate_competition.md:
--------------------------------------------------------------------------------
1 | # 1:1:1 competition simulation
2 | 
3 | [Return to tutorials](tutorial.md)
4 | Competition is often used in assays, utilising displacement of a labelled ligand by new inhibitor to detect competitive binding and displacement of the label. Example code is available here:
5 | https://github.com/stevenshave/pybindingcurve/blob/master/example_competition_simulation.py
6 |
7 | First perform imports:
8 | ```python
9 | import numpy as np
10 | import pybindingcurve as pbc
11 | ```
12 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow.
13 |
14 | Create the PBC BindingCurve object, governed by a 'competition' system:
15 | ```python
16 | mySystem = pbc.BindingCurve("competition")
17 | ```
18 | First, let’s simulate a curve with no inhibitor present (essentially 1:1)
19 |
20 | ```python
21 | mySystem.add_curve(p": np.linspace(0, 40, 20), "l": 10, "i": 0, "kdpi": 1, "kdpl": 10}, "No inhibitor")
22 | ```
23 | Add curve with inhibitor (i):
24 | ```python
25 | mySystem.add_curve(
26 | {"p": np.linspace(0, 40, 20), "l": 10, "i": 10, "kdpi": 1, kdpl": 10}, "[i] = 10 µM"
27 | )
28 | ```
29 |
30 | Add curve with more inhibtor (i):
31 | ```python
32 | mySystem.add_curve(
33 | {"p": np.linspace(0, 40, 20), "l": 10, "i": 25, "kdpi": 1, "kdpl": 10}, "[i] = 25 µM"
34 | )
35 | ```
36 | Display the plot:
37 |
38 | ```python
39 | mySystem.show_plot()
40 | ```
41 | Which results in the following:
42 |
43 | 
44 |
45 |
46 | [Return to tutorials](tutorial.md)
--------------------------------------------------------------------------------
/docs/simulate_custom_system.md:
--------------------------------------------------------------------------------
1 | # Simulation of custom binding systems
2 | 
3 |
4 | [Return to tutorials](tutorial.md)
5 |
6 | Example code for a simple binding system is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_custom_binding_system.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_custom_binding_system.py)
7 |
8 | A more complex example is also available here:
9 | [https://github.com/stevenshave/pybindingcurve/blob/master/example_custom_binding_system2.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_custom_binding_system2.py)
10 |
11 | PyBindingCurve is able to write custom functions representing a binding system from very simple system definition strings. This allows the simple definition, solving, plotting and fitting to any custom system.
12 |
13 | We define these custom systems as simple strings with reactions separated either on newlines, commas, or a combination of the two. Reactions take the form:
14 |
15 | - r1+r2<->p
16 |
17 | Denoting that reactant1 + reactant2 bind together to make the product.
18 |
19 | PBC will generate equations representing the custom system and use root finding techniques to calculate species concentrations at equilibrium. Readouts are signified by inclusion of a star (*) on a species. If no star is found, then the first seen product is
20 | used. Some system examples follow:
21 | - "P+L<->PL" - standard protein-ligand binding
22 | - "P+L<->PL, P+I<->PI" - competition binding
23 | - "P+P<->PP" - dimer formation (default readout on PP - dimer)
24 | - "P*+P<->PP" - dimer formation (readout specified on P - monomer)
25 | - "monomer+monomer<->dimer" - dimer formation (default readout on PP)
26 | - "P+L<->PL1, P+L<->PL2, PL1+L<->PL1L2, PL2+L<->PL1L2" - 1:2 site binding
27 |
28 | KDs passed to custom systems use underscores to separate species. P+L<->PL would require the KD passed as kd_p_l_pl. Running with incomplete system
29 | parameters will prompt for the correct ones. All species and KDs are cast to lower-case, simplifying parameter passing.
30 |
31 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations and KDs bellow.
32 |
33 | To simulate a highly complex system, where protein binds to ligand, but protein can dimerize, while protein dimer binds to an inhibitor, and the protein dimer has can bind a single ligand, and our readout is on the protein monomer bound to ligand, we would define the system as follows:
34 |
35 | ```python
36 | custom_system="""
37 | P+L<->PL*
38 | P+P<->PP
39 | PP+I<->PPI
40 | PPI+L<->PPIL
41 | """
42 | ```
43 | We can simulate this system in Python as follows:
44 |
45 | ```python
46 | import numpy as np
47 | import pybindingcurve as pbc
48 | custom_system="""
49 | P+L<->PL*
50 | P+P<->PP
51 | PP+I<->PPI
52 | PPI+L<->PPIL
53 | """
54 | my_system = pbc.BindingCurve(custom_system)
55 | ```
56 |
57 | We then define the system parameters in a python dictionary.
58 |
59 | ```python
60 | system_parameters = {
61 | 'p': np.linspace(0,5),
62 | 'l':0.5,
63 | 'i':4.0,
64 | 'kd_p_l_pl':4.3,
65 | 'kd_p_p_pp':1.2,
66 | 'kd_pp_i_ppi':1.2,
67 | 'kd_ppi_l_ppil':0.2,
68 | }
69 | ```
70 | We can now add the curve to the plot. If we want multiple simulations on the same plot, then it is good to give the curve a name with the optional name parameter.
71 | ```python
72 | my_system.add_curve(system_parameters, name= “Curve 1”)
73 | ```
74 |
75 | Finally show the plot. Optionally, title, xlabel and ylabel variables may also be passed to title the plot and axes.
76 | ```python
77 | my_system.show_plot()
78 | ```
79 | This produces the following plot:
80 | 
81 |
82 | To obtain exact single points from the plot, we may call the query function of my_system.
83 |
84 | If a list or NumPy array is included as a system parameter, then a NumPy array of results is returned.
85 |
86 | We may want to simulate a system in terms of a signal, not the concentration of complex. In this case, we may pass additional parameters, setting the ymax and/or ymin variables in the system parameters. Inclusion of these will scale the signal present between these values. This is very important if a detector is used with a maximum or minimum sensitivity and you wish to simulate response.
87 |
88 | [Return to tutorials](tutorial.md)
--------------------------------------------------------------------------------
/docs/simulate_homodimerbreaking.md:
--------------------------------------------------------------------------------
1 | # Simulation of homodimer breaking
2 | 
3 | [Return to tutorials](tutorial.md)
4 |
5 | Homodimer breaking is like a competition experiment, however breaking of the dimer results in two monomers. Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_simulation.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_simulation.py)
6 |
7 | First, we perform the standard imports:
8 |
9 | ```python
10 | import numpy as np
11 | import pybindingcurve as pbc
12 | ```
13 |
14 | We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow. Define out homodimer breaking system, titrating in inhibitor:
15 |
16 | ```python
17 | system_parameters = {"p": 30, "kdpp": 10, "i": np.linspace(0,60), "kdpi": 1}
18 | ```
19 |
20 | Create the PBC BindingCurve object, expecting a 'homodimer breaking' system:
21 |
22 | ```python
23 | my_system = pbc.BindingCurve("homodimer breaking")
24 | ```
25 | Add the system to PBC, generating a plot and display it:
26 |
27 | ```python
28 | my_system.add_curve(system_parameters)
29 | my_system.show_plot()
30 | ```
31 | Resulting in:
32 | 
33 |
34 |
35 | [Return to tutorials](tutorial.md)
--------------------------------------------------------------------------------
/docs/simulate_homodimerformation.md:
--------------------------------------------------------------------------------
1 | # Homodimer formation simulation
2 | 
3 |
4 | [Return to tutorials](tutorial.md)
5 |
6 | Homodimer formation is a very simple, with only one species present, two units of which bind together to make one of a new species (dimer). Dissociation of the dimer then makes two monomers.
7 | Example code is available here: [https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_simulation.py](https://github.com/stevenshave/pybindingcurve/blob/master/example_homodimer_formation_simulation.py)
8 |
9 | First, we perform the standard imports:
10 |
11 | ```python
12 | import numpy as np
13 | import pybindingcurve as pbc
14 | ```
15 |
16 | Define our system, homodimer formation has only p (protein, or monomer concentration) and kdpp (the dissociation constant of the dimer). We can choose to work in a common unit, typically nM, or µM, as long as all numbers are in the same unit, the result is valid. We assume µM for all concentrations bellow:
17 |
18 | ```python
19 | system_parameters = {"p": np.linspace(0, 10), "kdpp": 10}
20 | ```
21 |
22 | Make a pbc BindingCurve defined by the 'homodimer formation' binding system:
23 |
24 | ```python
25 | mySystem = pbc.BindingCurve("homodimer formation")
26 | ```
27 |
28 | We can now add the curve to the plot and show it:
29 |
30 | ```python
31 | mySystem.add_curve(system_parameters)
32 | mySystem.show_plot()
33 | ```
34 |
35 | This produces the following simulation plot of the theoretical experiment. It is theoretical as monomer is titrated in, with no dimer present, something not achievable, although it could be done in reverse:
36 | 
37 |
38 |
39 | [Return to tutorials](tutorial.md)
40 |
--------------------------------------------------------------------------------
/docs/tutorial.md:
--------------------------------------------------------------------------------
1 | # PyBindingCurve tutorials
2 | The following pages contain tutorials for simulation and fitting to a number of different systems. The most extensive, with most explanation is simple 1:1 simulation and fitting. Tutorials for the following systems are available:
3 |
4 |
5 | ## 1:1
6 | 
7 | - [Simulation](simulate_1to1.md)
8 | - [Fitting](fit_1to1.md)
9 |
10 |
11 | ## 1:1:1 Competition
12 | 
13 | - [Simulation](simulate_competition.md)
14 | - [Fitting](fit_competition.md)
15 |
16 |
17 | ## Homodimer formation
18 | 
19 | - [Simulation](simulate_homodimerformation.md)
20 | - [Fitting](fit_homodimerformation.md)
21 |
22 |
23 | ## Homodimer breaking
24 | 
25 | - [Simulation](simulate_homodimerbreaking.md)
26 | - [Fitting](fit_homodimerbreaking.md)
27 |
28 |
29 | ## Custom system
30 | 
31 | - [Simulation](simulate_custom_system.md)
32 |
33 | [Return to main site](index.md)
--------------------------------------------------------------------------------
/email-address-image.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/email-address-image.gif
--------------------------------------------------------------------------------
/example_1to1_fit.py:
--------------------------------------------------------------------------------
1 | """Fitting example, determining Kd from 1:1 binding data"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 |
6 | # We can choose to work in a common unit, typically nM, or uM, as long as all
7 | # numbers are in the same unit, the result is valid. We assume uM for all
8 | # concentrations bellow.
9 |
10 | # Experimental data
11 | xcoords = np.array(
12 | [0.0, 20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0]
13 | )
14 | ycoords = np.array(
15 | [0.544, 4.832, 6.367, 7.093, 7.987, 9.005, 9.079, 8.906, 9.010, 10.046, 9.225]
16 | )
17 |
18 | # Construct the PyBindingCurve object, operating on a simple 1:1 system and
19 | # add experimental data to the plot
20 | my_system = pbc.BindingCurve("1:1")
21 | my_system.add_scatter(xcoords, ycoords)
22 |
23 | # Known system parameters, kdpl will be added to this by fitting
24 | system_parameters = {"p": xcoords, "l": 10}
25 |
26 | # Now we call fit, passing the known parameters, followed by a dict of
27 | # parameters to be fitted along with an initial guess, pass the ycoords, and
28 | # what the readout (ycoords) is
29 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpl": 0}, ycoords)
30 |
31 | # Print out the fitted parameters
32 | for k, v in fit_accuracy.items():
33 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
34 |
35 | # Assign more points to 'p' to make a smooth plot
36 | fitted_system["p"] = np.linspace(0, np.max(xcoords), num=200)
37 |
38 | # Add a new curve, simulated using fitted parameters to our BindingCurve object
39 | my_system.add_curve(fitted_system)
40 |
41 | # Show the plot
42 | my_system.show_plot(ylabel="Signal")
43 |
--------------------------------------------------------------------------------
/example_1to1_simulation.py:
--------------------------------------------------------------------------------
1 | """Simulation example 1:1 binding"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 |
6 | # Define our system, 1:1 binding has p and l which (usually) relate two protein
7 | # and ligand concentration, although can be any two species which bind. kdpl
8 | # is the dissociation constant between the two species.
9 |
10 | # We can choose to work in a common unit, typically nM, or uM, as long as all
11 | # numbers are in the same unit, the result is valid. We assume uM for all
12 | # concentrations bellow.
13 | system_parameters = {"p": 1, "l": 10, "kdpl": 1}
14 |
15 | # Make a pbc BindingCurve defined by the simple 1:1 binding system
16 | my_system = pbc.BindingCurve("1:1")
17 | print("Simulating 1:1 binding system with these parameters:")
18 | print(system_parameters)
19 | print("pl=", my_system.query(system_parameters))
20 |
21 | # Simulate and visualise a binding curve.
22 | # First, we redefine the system parameters so that one variable is changing
23 | # in this case, we choose protein, performing a titration from 0 to 10 uM.
24 | system_parameters = {"p": np.linspace(0, 20), "l": 10, "kdpl": 1}
25 |
26 | # We can now add the curve to the plot, name it with an optional name= value.
27 | my_system.add_curve(system_parameters)
28 |
29 | # Lets change the KD present in the system parameters into something higher
30 | # affinity (lower KD) and add it to the curve
31 | system_parameters2 = {"p": np.linspace(0, 20), "l": 10, "kdpl": 0.5}
32 | my_system.add_curve(system_parameters2)
33 |
34 | # Show the plot
35 | my_system.show_plot()
36 |
--------------------------------------------------------------------------------
/example_competition_fit.py:
--------------------------------------------------------------------------------
1 | """Fitting example, determining Kd from 1:1:1 competition data"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import sys
6 |
7 | # We can choose to work in a common unit, typically nM, or uM, as long as all
8 | # numbers are in the same unit, the result is valid. We assume uM for all
9 | # concentrations bellow.
10 |
11 | # Experimental data
12 | xcoords = np.array([0.0, 4.2, 8.4, 16.8, 21.1, 31.6, 35.8, 40.0])
13 | ycoords = np.array([150, 330, 1050, 3080, 4300, 6330, 6490, 6960])
14 |
15 | # Construct the PyBindingCurve object, operating on a 1:1:1 (compeittion) system and add experimental data to the plot
16 | my_system = pbc.BindingCurve("1:1:1")
17 | my_system.add_scatter(xcoords, ycoords)
18 |
19 | # Known system parameters, kdpl will be added to this by fitting
20 | system_parameters = {
21 | "p": xcoords,
22 | "l": 10,
23 | "i": 10,
24 | "kdpl": 10,
25 | "ymin": np.min(ycoords),
26 | }
27 |
28 | # Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along
29 | # with an initial guess, pass the ycoords, and what the readout (ycoords) is
30 | fitted_system, fit_accuracy = my_system.fit(
31 | system_parameters, {"kdpi": 0, "ymax": np.max(ycoords)}, ycoords
32 | )
33 |
34 | # Print out the fitted parameters
35 | for k, v in fit_accuracy.items():
36 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
37 |
38 | # Assign more points to 'p' to make a smooth plot
39 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
40 |
41 | # Add a new curve, simulated using fitted parameters to our BindingCurve object
42 | my_system.add_curve(fitted_system)
43 |
44 | # Show the plot
45 | my_system.show_plot(ylabel="Signal")
46 |
--------------------------------------------------------------------------------
/example_competition_simulation.py:
--------------------------------------------------------------------------------
1 | """Simulation example 1:1:1 comptition binding"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 |
6 | # We can choose to work in a common unit, typically nM, or uM, as long as all
7 | # numbers are in the same unit, the result is valid. We assume uM for all
8 | # concentrations bellow.
9 |
10 | # Create the PBC BindingCurve object, expecting a 'competition' system.
11 | my_system = pbc.BindingCurve("competition")
12 |
13 | # First, lets simulate a curve with no inhibitor present (essentially 1:1)
14 | my_system.add_curve(
15 | {"p": np.linspace(0, 40, num=200), "l": 0.01, "i": 0, "kdpi": 1, "kdpl": 10},
16 | "No inhibitor",
17 | )
18 |
19 | # Add curve with more inhibtor (i)
20 | my_system.add_curve(
21 | {"p": np.linspace(0, 40, num=200), "l": 0.01, "i": 10, "kdpi": 10, "kdpl": 10},
22 | "[i] = 25 uM",
23 | )
24 |
25 | # Add curve with inhibitor (i)
26 | my_system.add_curve(
27 | {"p": np.linspace(0, 40, num=200), "l": 0.01, "i": 10, "kdpi": 0.5, "kdpl": 10},
28 | "[i] = 10 µM",
29 | )
30 |
31 |
32 | # Add curve with inhibitor (i)
33 | my_system.add_curve(
34 | {"p": np.linspace(0, 40, num=200), "l": 0.01, "i": 10, "kdpi": 0.1, "kdpl": 10},
35 | "[i] = 10 µM",
36 | )
37 | # Add curve with inhibitor (i)
38 | my_system.add_curve(
39 | {"p": np.linspace(0, 40, num=200), "l": 0.01, "i": 10, "kdpi": 0.01, "kdpl": 10},
40 | "[i] = 10 µM",
41 | )
42 | # Display the plot
43 | my_system.show_plot()
44 |
--------------------------------------------------------------------------------
/example_custom_binding_system.py:
--------------------------------------------------------------------------------
1 | """Simulation example, custom binding system
2 |
3 | PyBindingCurve allows the use of custom binding systems derived from a simple
4 | syntax. This is in the form of a string with reactions separated either on
5 | newlines, commas, or a combination of the two. Reactions take the form:
6 | r1+r2<->p - denoting reactant1 + reactant2 form p. PBC will generate and
7 | solve custom systems. Readouts are signified by inclusion of a
8 | star (*) on a species. If no star is found, then the first seen product is
9 | used. Some system examples follow:
10 | -> "P+L<->PL" - standard protein-ligand binding
11 | -> "P+L<->PL, P+I<->PI" - competition binding
12 | -> "P+P<->PP" - dimer formation
13 | -> "monomer+monomer<->dimer" - dimer formation
14 | -> "P+L<->PL1, P+L<->PL2, PL1+L<->PL1L2, PL2+L<->PL1L2" - 1:2 site binding
15 | All species are cast to lowercase and parameters such as concentrations should
16 | be passed as lower case - like 'p':10 for a starting protein concentration of
17 | 10 µM (depending on standard unit used)
18 | KDs passed to custom systems use underscores to separate species and product
19 | and prefixed with kd. P+L<->PL would require the KD passed as kd_p_l_pl.
20 | Running with incomplete system parameters will prompt for the correct ones.
21 | The methods employed to solve the system will return a result accurate to 10
22 | decimal places.
23 | """
24 |
25 | import numpy as np
26 | import pybindingcurve as pbc
27 |
28 | # Define the custom system
29 | custom_system = "P+L<->PL*"
30 |
31 | # Make a pbc BindingCurve defined by the custom system string above
32 | my_system = pbc.BindingCurve(custom_system)
33 |
34 | # We interrogate the system to see which parameters/arguments are required
35 | # for calculation
36 | print("Required parameters/arguments are: ", my_system.get_system_arguments())
37 |
38 | # We can choose to work in a common unit, typically nM, or uM, as long as all
39 | # numbers are in the same unit, the result is valid. We assume uM for all
40 | # concentrations bellow.
41 | system_parameters = {"p": np.linspace(0, 20), "l": 5, "kd_p_l_pl": 1.2}
42 |
43 | # We can now add the curve to the plot, name it with an optional name= value.
44 | my_system.add_curve(system_parameters)
45 |
46 | # Show the plot
47 | my_system.show_plot()
48 |
--------------------------------------------------------------------------------
/example_custom_binding_system2.py:
--------------------------------------------------------------------------------
1 | """Simulation example, custom binding system2 - induced sandwich formation
2 |
3 | PyBindingCurve allows the use of custom binding systems derived from a simple
4 | syntax. This is in the form of a string with reactions separated either on
5 | newlines, commas, or a combination of the two. Reactions take the form:
6 | r1+r2<->p - denoting reactant1 + reactant2 form p. PBC will generate and
7 | solve custom systems. Readouts are signified by inclusion of a
8 | star (*) on a species. If no star is found, then the first seen product is
9 | used. Some system examples follow:
10 | -> "P+L<->PL" - standard protein-ligand binding
11 | -> "P+L<->PL, P+I<->PI" - competition binding
12 | -> "P+P<->PP" - dimer formation
13 | -> "monomer+monomer<->dimer" - dimer formation
14 | -> "P+L<->PL1, P+L<->PL2, PL1+L<->PL1L2, PL2+L<->PL1L2" - 1:2 site binding
15 | All species are cast to lowercase and parameters such as concentrations should
16 | be passed as lower case - like 'p':10 for a starting protein concentration of
17 | 10 µM (depending on standard unit used)
18 | KDs passed to custom systems use underscores to separate species and product
19 | and prefixed with kd. P+L<->PL would require the KD passed as kd_p_l_pl.
20 | Running with incomplete system parameters will prompt for the correct ones.
21 | The methods employed to solve the system will return a result accurate to 10
22 | decimal places.
23 |
24 | The simulation bellow is used in the supporting information for the paper,
25 | exemplifying ternary system formation using the Cdc34-Cc0651-Ubiquitin complex
26 | as an example. See paper supporting information - 'Custom binding system
27 | example 2' for further diagramatic explanation and references.
28 | """
29 |
30 | import numpy as np
31 | import pybindingcurve as pbc
32 |
33 | # Define the custom system
34 |
35 | custom_system = """
36 | P+C<->PC
37 | P+U<->PU
38 | PC+U<->PCU
39 | PU+C<->PCU*
40 | """
41 |
42 |
43 | # Make a pbc BindingCurve defined by the custom system string above
44 | my_system = pbc.BindingCurve(custom_system)
45 |
46 | # We interrogate the system to see which parameters/arguments are required
47 | # for calculation
48 | print("Required parameters/arguments are: ", my_system.get_system_arguments())
49 |
50 | # We can choose to work in a common unit, typically nM, or uM, as long as all
51 | # numbers are in the same unit, the result is valid. We assume uM for all
52 | # concentrations bellow.
53 |
54 | system_parameters = {
55 | "p": 5,
56 | "c": np.linspace(0, 200),
57 | "u": 50,
58 | "kd_p_c_pc": 250,
59 | "kd_pc_u_pcu": 14,
60 | "kd_p_u_pu": 1000,
61 | "kd_pu_c_pcu": 14,
62 | }
63 |
64 | # We can now add the curve to the plot, name it with an optional name= value.
65 | my_system.add_curve(system_parameters)
66 |
67 | # Show the plot
68 | my_system.show_plot()
69 |
--------------------------------------------------------------------------------
/example_homodimer_breaking_fit.py:
--------------------------------------------------------------------------------
1 | """Fitting example, determining Kd for homodimer breaking data"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import sys
6 |
7 | # We can choose to work in a common unit, typically nM, or uM, as long as all
8 | # numbers are in the same unit, the result is valid. We assume uM for all
9 | # concentrations bellow.
10 |
11 | # Experimental data
12 | xcoords = np.array([0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0])
13 | ycoords = np.array([0, 0.00680125560, 0.935183266, 3.49, 6.9, 10.7, 14.7])
14 |
15 | # Construct the PyBindingCurve object, operating on a homodimer breaking system and add experimental data to the plot
16 | my_system = pbc.BindingCurve("homodimer breaking")
17 | my_system.add_scatter(xcoords, ycoords)
18 |
19 | # Known system parameters, kdpl will be added to this by fitting
20 | system_parameters = {"p": xcoords, "i": 20, "kdpp": 10}
21 |
22 | # Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along
23 | # with an initial guess, pass the ycoords, and what the readout (ycoords) is
24 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpi": 1}, ycoords)
25 |
26 | # Print out the fitted parameters
27 | for k, v in fit_accuracy.items():
28 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
29 |
30 | # Assign more points to 'p' to make a smooth plot
31 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
32 |
33 | # Add a new curve, simulated using fitted parameters to our BindingCurve object
34 | my_system.add_curve(fitted_system)
35 |
36 | # Show the plot
37 | my_system.show_plot()
38 |
--------------------------------------------------------------------------------
/example_homodimer_breaking_simulation.py:
--------------------------------------------------------------------------------
1 | """Simulation example homodimer breaking"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import time
6 |
7 | # We can choose to work in a common unit, typically nM, or uM, as long as all
8 | # numbers are in the same unit, the result is valid. We assume uM for all
9 | # concentrations bellow.
10 |
11 | # Define our homodimer breaking system, titrating in inhibitor
12 | system_parameters = {"p": 30, "kdpp": 10, "i": np.linspace(0, 60), "kdpi": 1}
13 |
14 | # Create the PBC BindingCurve object, expecting a 'homodimer breaking' system.
15 | my_system = pbc.BindingCurve("homodimer breaking")
16 |
17 | # Add the system to PBC, generating a plot.
18 | my_system.add_curve(system_parameters)
19 |
20 | # Display the plot
21 | my_system.show_plot()
22 |
--------------------------------------------------------------------------------
/example_homodimer_formation_fit.py:
--------------------------------------------------------------------------------
1 | """Fitting example, determining Kd for homodimer breaking data"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import sys
6 |
7 | # We can choose to work in a common unit, typically nM, or uM, as long as all
8 | # numbers are in the same unit, the result is valid. We assume uM for all
9 | # concentrations bellow.
10 |
11 | # Experimental data
12 | xcoords = np.array([0.0, 2, 4, 6, 8, 10])
13 | ycoords = np.array([0.0, 0.22, 0.71, 1.24, 1.88, 2.48])
14 |
15 | # Construct the PyBindingCurve object, operating on a homodimer formation
16 | # system and add experimental data to the plot
17 | my_system = pbc.BindingCurve("homodimer formation")
18 | my_system.add_scatter(xcoords, ycoords)
19 |
20 | # Known system parameters, kdpp will be added to this by fitting
21 | system_parameters = {"p": xcoords}
22 |
23 | # Now we call fit, passing the known parameters, followed by a dict of parameters to be fitted along
24 | # with an initial guess, pass the ycoords, and what the readout (ycoords) is
25 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpp": 0}, ycoords)
26 |
27 | # Print out the fitted parameters
28 | for k, v in fit_accuracy.items():
29 | print(f"Fit: {k}={fitted_system[k]} +/- {v}")
30 |
31 | # Assign more points to 'p' to make a smooth plot
32 | fitted_system["p"] = np.linspace(0, np.max(xcoords))
33 |
34 | # Add a new curve, simulated using fitted parameters to our BindingCurve object
35 | my_system.add_curve(fitted_system)
36 |
37 | # Show the plot
38 | my_system.show_plot()
39 |
--------------------------------------------------------------------------------
/example_homodimer_formation_simulation.py:
--------------------------------------------------------------------------------
1 | """Simulation example homodimer formation"""
2 |
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import time
6 |
7 | # Define our system, homodimer formation has only:
8 | # p: protein, or monomer concentration
9 | # kdpp: the dissociation constant between the two species.
10 | # We can choose to work in a common unit, typically nM, or uM, as long as all
11 | # numbers are in the same unit, the result is valid. We assume uM for all
12 | # concentrations bellow.
13 |
14 |
15 | # Define the system
16 | system_parameters = {"p": np.linspace(0, 10), "kdpp": 10}
17 |
18 | # Make a pbc BindingCurve defined by the 'homodimer formation' binding system
19 | my_system = pbc.BindingCurve("homodimer formation")
20 |
21 | # We can now add the curve to the plot, name it with an optional name= value.
22 | my_system.add_curve(system_parameters)
23 | print(my_system.curves[0].ycoords)
24 | # Show the plot
25 | my_system.show_plot()
26 |
--------------------------------------------------------------------------------
/example_paper_figure1.py:
--------------------------------------------------------------------------------
1 | """
2 | Generate paper figure 1
3 |
4 | Figure 1 in the PyBindingCurve paper consists of 2 plots in one panel,
5 | the first illustrating the dimer formation rates as a function of monomer
6 | concentration, and the other titration of an inhibitor into these preformed
7 | complexes.
8 | """
9 |
10 | import matplotlib as mpl
11 | import numpy as np
12 | import pybindingcurve as pbc
13 | import matplotlib.pyplot as plt
14 | import pickle
15 | import matplotlib.ticker
16 | import sys
17 |
18 | # We can choose to work in a common unit, typically nM, or uM, as long as all
19 | # numbers are in the same unit, the result is valid. We assume nM for all
20 | # concentrations bellow, but divide results by 1000 on plotting to convert to
21 | # a more convenient µM representation.
22 | num_points = 500 # Number of points on the two x-axes
23 | max_inhibitor_concentration = 5000
24 | maximum_monomer_concentration = 1000
25 | dimer_kd = 100 # 100 nM dimer KDs
26 | inhibitor_kd = 10 # 10 nM inhibitor KDs
27 |
28 | # A reviewer enquired as to why the x-axes of plots in figure 1 were not
29 | # using a log scale. In testing, we found this obscured the critical crossover
30 | # points discussed in the text. To enable this log scale on x-axes, set the
31 | # bellow variable to True, the most insightful figure is achieved with 'False'
32 | use_log_xaxis_scale=False
33 |
34 | # Calculate formation concentrations
35 | x_axis_formation = np.linspace(0, maximum_monomer_concentration, num=num_points)
36 | x_axis_breaking = np.linspace(0, max_inhibitor_concentration, num=num_points)
37 | homo_y_formation = np.empty((num_points))
38 | hetero_y_formation = np.empty((num_points))
39 |
40 | # Make the formation and breaking PBC binding curves that we will later query
41 | pbc_homodimer_formation = pbc.BindingCurve("homodimer formation")
42 | pbc_heterodimer_formation = pbc.BindingCurve(
43 | "1:1"
44 | ) # Heterodimer formation is just the same as 1:1, or P+L<->PL
45 | pbc_homodimer_breaking = pbc.BindingCurve("homodimer breaking")
46 | pbc_heterodimer_breaking = pbc.BindingCurve("competition")
47 |
48 | # Perform the calculations
49 | homodimer_formation_concs = pbc_homodimer_formation.query(
50 | {"kdpp": dimer_kd, "p": x_axis_formation * 2}
51 | )
52 | homodimer_breaking_concs = pbc_homodimer_breaking.query(
53 | {"kdpi": inhibitor_kd, "kdpp": dimer_kd, "p": 2000, "i": x_axis_breaking}
54 | )
55 | heterodimer_breaking_concs = pbc_heterodimer_breaking.query(
56 | {"kdpi": inhibitor_kd, "kdpl": dimer_kd, "p": 1000, "l": 1000, "i": x_axis_breaking}
57 | )
58 | # Because heterodimer formation requires 2 chaning parameters at once, we must
59 | # do it in a loop of single queries. Breaking does not require such treatment
60 | # as only the inhibitor concentration changes.
61 | heterodimer_formation_concs = np.empty((len(x_axis_formation)))
62 | for i, monomer_conc in enumerate(x_axis_formation):
63 | heterodimer_formation_concs[i] = pbc_heterodimer_formation.query(
64 | {"kdpl": dimer_kd, "p": monomer_conc, "l": monomer_conc}
65 | )
66 |
67 |
68 | fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 5.5), sharey=True)
69 | fig.suptitle("Homo- vs Hetero-dimer", fontsize=18)
70 |
71 | ax[0].plot(
72 | x_axis_formation / 1000,
73 | homodimer_formation_concs / 1000,
74 | "k",
75 | label="Homodimer monomers",
76 | linestyle="--",
77 | )
78 | ax[0].plot(
79 | x_axis_formation / 1000,
80 | heterodimer_formation_concs / 1000,
81 | "k",
82 | label="Heterodimer monomers",
83 | )
84 | if not use_log_xaxis_scale:
85 | ax[0].set_xlim(0, maximum_monomer_concentration / 1000)
86 | ax[0].set_ylim(0, 1)
87 | ax[0].legend()
88 | ax[0].set_xlabel(r"[Monomers] ($\mathrm{\mu}$M)", fontsize=14)
89 | ax[0].set_ylabel(r"[Dimer] ($\mathrm{\mu}$M)", fontsize=14)
90 | ax[0].set_title(
91 | r"""Dimer formation,
92 | Dimer K$\mathrm{_D}$s = 100 nM""",
93 | fontsize=14,
94 | )
95 | ax[0].grid()
96 |
97 | ax[1].plot(
98 | x_axis_breaking / 1000,
99 | homodimer_breaking_concs / 1000,
100 | "k",
101 | label=r"2 $\mathrm{\mu}$M homodimer monomer",
102 | linestyle="--",
103 | )
104 | ax[1].plot(
105 | x_axis_breaking / 1000,
106 | heterodimer_breaking_concs / 1000,
107 | "k",
108 | label=r"1 $\mathrm{\mu}$M heterodimer monomers",
109 | )
110 | if not use_log_xaxis_scale:
111 | ax[1].set_xlim(0, max_inhibitor_concentration / 1000)
112 | ax[1].set_ylim(0, 1)
113 | ax[1].legend()
114 | ax[1].set_xlabel(r"[I$_0$] ($\mathrm{\mu}$M)", fontsize=14)
115 | # ax[1].set_ylabel(r"[Dimer] ($\mathrm{\mu}$M)", fontsize=14)
116 | ax[1].set_title(
117 | r"""Dimer breaking with inhibitor,
118 | Dimer K$\mathrm{_D}$s = 100 nM, inhibitor K$\mathrm{_D}$=10 nM""",
119 | fontsize=14,
120 | )
121 | if use_log_xaxis_scale:
122 | ax[0].set_xscale("log")
123 | ax[1].set_xscale("log")
124 | ax[1].grid()
125 | plt.tight_layout(rect=(0, 0, 1, 0.9425), w_pad=-0.4)
126 |
127 | for axis in ax:
128 | axis.tick_params(axis="both", which="major", labelsize=14)
129 | axis.tick_params(axis="both", which="minor", labelsize=14)
130 | fig.text(0.05, 0.87, "A)", fontsize=20)
131 | fig.text(0.514, 0.87, "B)", fontsize=20)
132 | plt.savefig("Figure1.svg")
133 | plt.savefig("Figure1.png", dpi=300)
134 | plt.subplots_adjust(top=0.829, wspace=0.08)
135 | plt.show()
136 |
--------------------------------------------------------------------------------
/example_paper_figure2.py:
--------------------------------------------------------------------------------
1 | """Generate paper figure 2
2 |
3 | Figure 2 in the PyBindingCurve paper consists of heatmaps showing
4 | homo- vs hetero-dimer breaking. The figure in BioRxiv preprint #1
5 | used numerically unstable code - before the inclusion of MPMath
6 | for arbitary precision arithmetic which fixes artefacts present in
7 | the old figure. First the code performs the long calculations,
8 | storing results in pickles, before rendering. Subsequent runs
9 | finding the pickles speeds up display.
10 | """
11 |
12 |
13 | from multiprocessing import Pool
14 | import numpy as np
15 | import pybindingcurve as pbc
16 | import pickle
17 | from pathlib import Path
18 | import seaborn as sns
19 | import matplotlib.pyplot as plt
20 |
21 |
22 | def get_2D_grid_values(
23 | xmin,
24 | xmax,
25 | ymin,
26 | ymax,
27 | system,
28 | parameters,
29 | x_parameter,
30 | y_parameter,
31 | filename,
32 | plot_steps,
33 | starting_y_guess=0,
34 | ):
35 | file = Path(filename)
36 | if file.exists():
37 | mat = pickle.load(open(filename, "rb"))
38 | return mat
39 | x_logconc = np.linspace(xmin, xmax, plot_steps)
40 | y_logconc = np.linspace(ymin, ymax, plot_steps)
41 | mat = np.ndarray((plot_steps, plot_steps))
42 | for ix, x in enumerate(x_logconc):
43 | print("Working on :", x)
44 | for iy, y in enumerate(y_logconc):
45 | parameters[x_parameter] = 10 ** x
46 | parameters[y_parameter] = 10 ** y
47 | mat[ix, iy] = system.query(parameters)
48 |
49 | pickle.dump(mat, open(filename, "wb"))
50 | return mat
51 |
52 |
53 | def generate_heatmaps(
54 | homodimer_map_file: Path, heterodimer_map_file: Path, plot_steps: int = 800
55 | ):
56 | pool = Pool(2)
57 | inhibitor_conc = 5.0
58 | pbc_homodimer_breaking = pbc.BindingCurve("homodimer breaking")
59 | pbc_heterodimer_breaking = pbc.BindingCurve("competition")
60 |
61 | print("Building homdimer heatmap file")
62 | j1 = pool.apply_async(
63 | get_2D_grid_values,
64 | [
65 | -3,
66 | 3,
67 | -3,
68 | 3,
69 | pbc_homodimer_breaking,
70 | {"p": 2, "i": inhibitor_conc},
71 | "kdpp",
72 | "kdpi",
73 | f"heatmaphomo-{str(inhibitor_conc)}.pkl",
74 | plot_steps,
75 | ],
76 | )
77 |
78 | print("Building heterodimer heatmap file")
79 |
80 | j2 = pool.apply_async(
81 | get_2D_grid_values,
82 | [
83 | -3,
84 | 3,
85 | -3,
86 | 3,
87 | pbc_heterodimer_breaking,
88 | {"p": 1, "l": 1, "i": inhibitor_conc},
89 | "kdpl",
90 | "kdpi",
91 | f"heatmaphetero-{str(inhibitor_conc)}.pkl",
92 | plot_steps,
93 | ],
94 | )
95 | res = j1.get()
96 | res = j2.get()
97 |
98 |
99 | def load_heatmap(filepath: Path):
100 | if filepath.exists():
101 | mat = pickle.load(open(filepath, "rb"))
102 | print(filepath, "shape=", mat.shape)
103 | # mat = mat[::2, ::2] # To downsample, if required for saved matrixq
104 | return mat
105 | else:
106 | return None
107 |
108 |
109 | def make_plot(homodimer_map_file: Path, heterodimer_map_file: Path):
110 | heatmap_homo = load_heatmap(homodimer_map_file)
111 | heatmap_hetero = load_heatmap(heterodimer_map_file)
112 |
113 | fig, ax = plt.subplots(ncols=3, figsize=(10, 4), sharey=True, sharex=True)
114 |
115 | colour_map = sns.color_palette("RdBu_r", 256)
116 |
117 | sns.heatmap(
118 | heatmap_homo[:, ::-1],
119 | ax=ax[0],
120 | vmin=0,
121 | vmax=1,
122 | cmap=colour_map,
123 | cbar=True,
124 | cbar_kws=dict(pad=0.01),
125 | )
126 | sns.heatmap(
127 | heatmap_hetero[:, ::-1],
128 | ax=ax[1],
129 | vmin=0,
130 | vmax=1,
131 | cmap=colour_map,
132 | cbar=True,
133 | cbar_kws=dict(pad=0.01),
134 | )
135 | sns.heatmap(
136 | heatmap_homo[:, ::-1] - heatmap_hetero[:, ::-1],
137 | ax=ax[2],
138 | cmap=sns.color_palette("PRGn", 1024),
139 | center=0,
140 | cbar=True,
141 | cbar_kws=dict(pad=0.01),
142 | )
143 |
144 | x_labels = ["9 (nM)", "6 ($\mathrm{\mu}$M)", "3 (mM)"]
145 | x_labels.reverse()
146 | y_labels = x_labels.copy()
147 |
148 | y_labels.reverse()
149 | y_labels[2] = " 3 (mM)"
150 | for axx in ax:
151 | plt.xticks(
152 | np.arange(0, heatmap_hetero.shape[1] + 1, heatmap_hetero.shape[1] / 2),
153 | rotation=0,
154 | )
155 | plt.yticks(
156 | np.arange(0, heatmap_hetero.shape[1] + 1, heatmap_hetero.shape[0] / 2)
157 | )
158 | axx.set_xticks = np.arange(len(x_labels))
159 | axx.set_yticks = np.arange(len(y_labels))
160 | axx.set_xticklabels(x_labels, rotation=0)
161 | axx.set_yticklabels(y_labels, rotation=0)
162 | axx.patch.set_linewidth("1")
163 | axx.patch.set_edgecolor("black")
164 |
165 | fig.text(0.45, 0.02, r"p$\mathrm{K_DI}$")
166 | fig.text(0.008, 0.445, r"p$\mathrm{K_DDimer}$", rotation=90)
167 | fig.text(0.165, 0.805, "Homodimer")
168 | fig.text(0.465, 0.805, "Heterodimer")
169 | fig.text(0.68, 0.805, "Difference (Homodimer-Heterodimer)")
170 |
171 | plt.suptitle(
172 | r"Dimer breaking with inhibitor, 2 $\mathrm{\mu}$M homodimer monomer and"
173 | + "\n"
174 | + r"1+1 $\mathrm{\mu}$M heterodimer monomers, 5 $\mathrm{\mu}$M inhibitor",
175 | fontsize=16,
176 | )
177 | plt.subplots_adjust(left=0.086, bottom=0.11, right=0.976, top=0.793, wspace=0.086)
178 |
179 | plt.savefig("Figure2.svg")
180 | plt.savefig("Figure2.png", dpi=300)
181 |
182 | plt.show()
183 |
184 |
185 | if __name__ == "__main__":
186 | # Be careful with the size of the matrices determined by plot_steps,
187 | # 800x800 matrices result in a ~360 MB SVG which crashes inkscape on
188 | # a 16GB machine. Downsampling by a factor of 2 in each dimension
189 | # produces a ~90 MB SVG which inkacape still struggles with.
190 | # Paper figure generated with plot_steps=400. The output PNG is
191 | # certainly easier to deal with than the SVG
192 |
193 | plot_steps = 400
194 | homodimer_map_file = Path(f"heatmaphomo-5.0.pkl")
195 | heterodimer_map_file = Path(f"heatmaphetero-5.0.pkl")
196 | if not homodimer_map_file.exists() or not heterodimer_map_file.exists():
197 | generate_heatmaps(
198 | homodimer_map_file, heterodimer_map_file, plot_steps=plot_steps
199 | )
200 | make_plot(homodimer_map_file, heterodimer_map_file)
201 |
--------------------------------------------------------------------------------
/interrogate_system_solutions.py:
--------------------------------------------------------------------------------
1 | import pybindingcurve as pbc
2 | import random
3 |
4 | output_file = open("system_results.csv", "w", buffering=1)
5 | output_file.write("p,l,kdpp,kdpl,solution")
6 | analytical_system = pbc.BindingCurve(pbc.systems.System_analytical_homodimerbreaking_pp)
7 | kinetic_system = pbc.BindingCurve(pbc.systems.System_kinetic_homodimerbreaking_pp)
8 |
9 | while True:
10 | query_system = {
11 | "p": random.uniform(0.0, 1000.0) * 10 ** random.choice([0, -3, -6, -9, -12]),
12 | "l": random.uniform(0.0, 1000.0) * 10 ** random.choice([0, -3, -6, -9, -12]),
13 | "kdpl": random.uniform(0.0, 1000.0) * 10 ** random.choice([0, -3, -6, -9, -12]),
14 | "kdpp": random.uniform(0.0, 1000.0) * 10 ** random.choice([0, -3, -6, -9, -12]),
15 | }
16 | kinetic_result = kinetic_system.query(query_system)
17 | analytical_result = analytical_system.query(query_system)
18 |
19 | closest = min(
20 | range(len(analytical_result)),
21 | key=lambda i: abs(analytical_result[i] - kinetic_result),
22 | )
23 | output_file.write(
24 | f"{query_system['p']},{query_system['l']},{query_system['kdpp']},{query_system['kdpl']},{closest}\n"
25 | )
26 |
27 | output_file.close()
28 |
--------------------------------------------------------------------------------
/pybindingcurve_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevenshave/pybindingcurve/4e592982848fea9207559a0c90ad47751e0e47be/pybindingcurve_logo.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name="pybindingcurve"
3 | version="1.2.3"
4 | description="Protein ligand binding simulation in Python"
5 | authors = [
6 | {name = "Steven Shave"},
7 | ]
8 | readme = "README.md"
9 | requires-python = ">=3.9"
10 | keywords = ["molecular similarity", "ligand based virtual screening"]
11 | dependencies = [
12 | "numpy>=1.26",
13 | "matplotlib>=3.8",
14 | "lmfit>=1.2.2",
15 | "mpmath>=1.3.0",
16 | "autograd>=1.6.2",
17 | ]
18 | [build-system]
19 | requires=[
20 | "setuptools>=68.2.2",
21 | ]
22 | build-backend="setuptools.build_meta"
23 |
24 | [tool.black]
25 | skip-string-normalization = true
26 | include = '''
27 | (
28 | ^/tests/
29 | | ^/src/
30 | | ^/setup[.]py
31 | )
32 | '''
33 | exclude = '''
34 | (
35 | __pycache__
36 | |.*\.egg-info
37 | )
38 | '''
39 |
40 | [tool.setuptools.packages.find]
41 | where = ["src"]
42 |
43 | [project.urls]
44 | "Homepage" = "https://github.com/stevenshave/pybindingcurve"
45 |
46 | [project.optional-dependencies]
47 | dev = [
48 | 'black==24.3.0',
49 | 'pytest==8.1.1',
50 | 'build==0.10.0',
51 | ]
52 |
53 | [tool.pytest.ini_options]
54 | "testpaths" = "tests/test*"
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup()
4 |
--------------------------------------------------------------------------------
/src/pybindingcurve/__init__.py:
--------------------------------------------------------------------------------
1 | from .pybindingcurve import *
2 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/__init__.py:
--------------------------------------------------------------------------------
1 | from .binding_system import BindingSystem
2 | from .analytical_systems import *
3 | from .lagrange_systems import *
4 | from .kinetic_systems import *
5 | from .minimizer_systems import *
6 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/analytical_systems.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from .analytical_equations import *
3 | from mpmath import mp
4 | from .binding_system import BindingSystem
5 |
6 |
7 | class System_analytical_one_to_one__pl(BindingSystem):
8 | """
9 | Analytical 1:1 binding system
10 |
11 | Class defines 1:1 binding, readout is PL
12 | See https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
13 | """
14 |
15 | def __init__(self):
16 | super().__init__(system01_analytical_one_to_one__pl, analytical=True)
17 | self.default_readout = "pl"
18 |
19 | def query(self, parameters: dict):
20 | mp.dps = 100
21 | if self._are_ymin_ymax_present(parameters):
22 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
23 | parameters
24 | )
25 | value = super().query(parameters_no_min_max)
26 | with np.errstate(divide="ignore", invalid="ignore"):
27 | return (
28 | parameters["ymin"]
29 | + ((parameters["ymax"] - parameters["ymin"]) * value)
30 | / parameters["l"]
31 | )
32 | else:
33 | return super().query(parameters)
34 |
35 |
36 | class System_analytical_competition__pl(BindingSystem):
37 | """
38 | Analytical 1:1:1 competition binding system
39 |
40 | Class defines 1:1:1 competition, readout is PL
41 | See https://stevenshave.github.io/pybindingcurve/simulate_competition.html
42 | """
43 |
44 | def __init__(self):
45 | super().__init__(system02_analytical_competition__pl, analytical=True)
46 | self.default_readout = "pl"
47 |
48 | def query(self, parameters: dict):
49 | mp.dps = 100
50 | if self._are_ymin_ymax_present(parameters):
51 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
52 | parameters
53 | )
54 | value = super().query(parameters_no_min_max)
55 | with np.errstate(divide="ignore", invalid="ignore"):
56 | return (
57 | parameters["ymin"]
58 | + ((parameters["ymax"] - parameters["ymin"]) * value)
59 | / parameters["l"]
60 | )
61 | else:
62 | return super().query(parameters)
63 |
64 |
65 | class System_analytical_homodimerformation__pp(BindingSystem):
66 | """
67 | Analytical homodimer formation system
68 |
69 | Class defines homodimer formation, readout is PP
70 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
71 | """
72 |
73 | def __init__(self):
74 | super().__init__(system03_analytical_homodimer_formation__pp, analytical=True)
75 | self.default_readout = "pp"
76 |
77 | def query(self, parameters: dict):
78 | mp.dps = 100
79 | if self._are_ymin_ymax_present(parameters):
80 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
81 | parameters
82 | )
83 | value = super().query(parameters_no_min_max)
84 | with np.errstate(divide="ignore", invalid="ignore"):
85 | return np.nan_to_num(
86 | parameters["ymin"]
87 | + ((parameters["ymax"] - parameters["ymin"]) * value)
88 | / (parameters["p"] / 2.0)
89 | )
90 | else:
91 | return super().query(parameters)
92 |
93 |
94 | class System_analytical_homodimerbreaking_pp(BindingSystem):
95 | """
96 | Analytical homodimer breaking system
97 |
98 | Class defines homodimer breaking, readout is PP
99 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
100 | """
101 |
102 | def __init__(self):
103 | super().__init__(
104 | system04_analytical_homodimer_breaking__pp,
105 | analytical=True,
106 | )
107 | self.default_readout = "pp"
108 | self.num_solutions = 2
109 |
110 | def query(self, parameters: dict):
111 | mp.dps = 100
112 | if self._are_ymin_ymax_present(parameters):
113 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
114 | parameters
115 | )
116 | value = super().query(parameters_no_min_max)
117 | with np.errstate(divide="ignore", invalid="ignore"):
118 | return np.nan_to_num(
119 | parameters["ymin"]
120 | + ((parameters["ymax"] - parameters["ymin"]) * value)
121 | / (parameters["p"] / 2.0)
122 | )
123 | else:
124 | return super().query(parameters)
125 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/binding_system.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | import numpy as np
3 | from mpmath import almosteq
4 |
5 | class BindingSystem:
6 | """
7 | BindingSystem class, used to determine the type of binding systems being used
8 |
9 | BindingSystem objects are governed with differnt protein-ligand binding
10 | system being represented. It also provides methods for visualisation,
11 | querying and the fitting of system parameters.
12 |
13 | Parameters
14 | ----------
15 | bindingsystem : func
16 | A function deriving the concentration of complex formed. This fuction
17 | may be varied based on different protein-ligand binding systems.
18 | analytical: bool
19 | Perform a analytical analysis (default = False)
20 | """
21 |
22 | system = None
23 | analytical = False
24 | arguments = []
25 | default_readout = None
26 |
27 | def _find_changing_parameters(self, params: dict):
28 | """
29 | Find the changing parameter
30 |
31 | Determine which parameter is changing with titration, including the
32 | concentration of the protein or ligand. A set of varied concentrations
33 | of parameter are in the form of array-like or list data type
34 |
35 | Parameters
36 | ----------
37 | params : dict
38 | Parameters defining the binding system to be simulated.
39 |
40 | Returns
41 | -------
42 | A list containing the keys of params indicating the name of
43 | changing parameters.
44 | """
45 | changing_list = []
46 | for p in params.keys():
47 | if isinstance(params[p], np.ndarray) or isinstance(params[p], list):
48 | changing_list.append(p)
49 | if len(changing_list) == 0:
50 | return None
51 | else:
52 | return changing_list
53 |
54 | def __init__(self, bindingsystem: callable, analytical: bool = False):
55 | """
56 | Construct BindingSystem objects
57 |
58 | BindingSystem objects are initialised with various subclasses under
59 | the BindingSystem super class definding different protein-ligand
60 | binding systems, such as 'System_analytical_one_to_one_pl' or
61 | 'System_analytical_homodimerformation_pp', etc.
62 |
63 | Parameters
64 | ----------
65 | bindingsystem : func
66 | A function deriving the concentration of complex formed. This
67 | fuction may be varied based on different protein-ligand binding
68 | systems.
69 | analytical: bool
70 | Perform a analytical analysis (default = False)
71 | """
72 | self._system = bindingsystem
73 | self.analytical = analytical
74 | self.arguments = list(signature(bindingsystem).parameters.keys())
75 |
76 | if not analytical and "interval" in self.arguments:
77 | # Make sure interval is not included in arguments
78 | self.arguments.remove("interval")
79 |
80 | def _remove_ymin_ymax_keys_from_dict_in_place(self, d: dict):
81 | """
82 | Remove minimum and maximum readout from the orignal system parameters
83 | (Replace the original dict)
84 |
85 | Output the original dict contains the orignal system parameters with
86 | removal the minimum and maximum readout signel of the system
87 |
88 | Parameters
89 | ----------
90 | d : dict
91 | Parameters defining the binding system to be simulated
92 |
93 | Returns
94 | -------
95 | dict
96 | Return the original dict contains original system parameters without
97 | minimum and maximum readout.
98 | """
99 | if "ymin" in d.keys():
100 | del d["ymin"]
101 | if "ymax" in d.keys():
102 | del d["ymax"]
103 | return d
104 |
105 | def _remove_ymin_ymax_keys_from_dict_return_new(self, input: dict):
106 | """
107 | Remove minimum and maximum readout from the orignal system parameters
108 | (Return a new dict)
109 |
110 | Output a new dict contains the orignal system parameters with removal
111 | the minimum and maximum readout signel of the system
112 |
113 | Parameters
114 | ----------
115 | input : dict
116 | Parameters defining the binding system to be simulated
117 |
118 | Returns
119 | -------
120 | dict
121 | Generate a new dict contains original system parameters without
122 | minimum and maximum readout.
123 | """
124 | d = dict(input)
125 | if "ymin" in d.keys():
126 | del d["ymin"]
127 | if "ymax" in d.keys():
128 | del d["ymax"]
129 | return d
130 |
131 | def query(self, parameters: dict):
132 | """
133 | Query a binding system
134 |
135 | Get the readout from from a set of system parameters
136 |
137 | Parameters
138 | ----------
139 | parameters : dict
140 | Parameters defining the binding system to be simulated.
141 |
142 | Returns
143 | -------
144 | Single floating point of the concentration of the binding complex, or
145 | array-like Response/signal of the system.
146 | """
147 | results = None
148 |
149 | # Check that all required parameters are present and abort if not.
150 | # Dont worry about all_concs which is used for multiple queries
151 | missing = sorted(
152 | list((set(self.arguments) - set(parameters.keys())) - set(["all_concs"]))
153 | )
154 | assert len(missing)==0, f"The following parameters were missing: {missing}"
155 |
156 | # Are any parameters changing?
157 | changing_parameters = self._find_changing_parameters(parameters)
158 | if changing_parameters is None: # Querying single point
159 | if self.analytical:
160 | results = self._system(**parameters) # Analytical
161 | else:
162 | simulation_results=self._system(**parameters)
163 | if self.default_readout not in simulation_results:
164 | self.default_readout=f"{self.default_readout}_f"
165 | results = simulation_results[self.default_readout]
166 |
167 | else:
168 | # At least 1 changing parameter
169 | if len(changing_parameters) == 1:
170 | results = None
171 | num_solutions = getattr(
172 | self, "num_solutions", 1
173 | ) # Get attribure of num_solutions in BindingSystem class (default = 1)
174 | if num_solutions == 1:
175 | results = np.empty(len(parameters[changing_parameters[0]]))
176 | else:
177 | results = np.empty(
178 | (len(parameters[changing_parameters[0]]), num_solutions)
179 | )
180 | # 1 changing parameter
181 | if self.analytical: # Using an analytical solution
182 | for i in range(results.shape[0]):
183 | tmp_params = dict(parameters)
184 | tmp_params[changing_parameters[0]] = parameters[
185 | changing_parameters[0]
186 | ][i]
187 | results[i] = self._system(**tmp_params)
188 | else: # Changing parameter on lagrange or kinetic solution
189 | for i in range(results.shape[0]):
190 | tmp_params = dict(parameters)
191 | tmp_params[changing_parameters[0]] = parameters[
192 | changing_parameters[0]
193 | ][i]
194 | simulation_results=self._system(**tmp_params)
195 | if self.default_readout not in simulation_results:
196 | self.default_readout=f"{self.default_readout}_f"
197 | results[i] = simulation_results[self.default_readout]
198 | else:
199 | print(
200 | "Only 1 parameter may change, currently changing: ",
201 | changing_parameters,
202 | )
203 | return None
204 | if isinstance(results, (np.ndarray)):
205 | if results.ndim > 1:
206 | results = results.T
207 | return np.nan_to_num(results)
208 | return results # We get here if its not a numpy array, but a system with multiple solutions queried at for a single point
209 |
210 | def get_all_species(self):
211 | if hasattr(self, "all_species"):
212 | return self.all_species
213 | else:
214 | raise NotImplementedError("get_all_species is not yet implemented for this type of binding system")
215 |
216 | def _are_ymin_ymax_present(self, parameters: dict):
217 | """
218 | Check the existance of the minimum or maximum readout
219 |
220 | Check the minimum or maximum readout signal set by user is in
221 | the binding system parameters
222 |
223 | Parameters
224 | ----------
225 | parameters : dict
226 | Parameters defining the binding system to be simulated.
227 |
228 | Returns
229 | -------
230 | Boolean (True or False)
231 | """
232 | if "ymin" in parameters.keys():
233 | if "ymax" in parameters.keys():
234 | return True
235 | else:
236 | print(
237 | "Warning: Only ymin was in parameters, missing ymax, setting ymax to 1.0"
238 | )
239 | parameters["ymax"] = 1.0
240 | return True
241 | else:
242 | if "ymax" in parameters.keys():
243 | print(
244 | "Warning: Only ymax was in parameters, missing ymin, setting ymin to 0.0"
245 | )
246 | parameters["ymin"] = 0.0
247 | return True
248 | return False
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/kinetic_systems.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from .binding_system import BindingSystem
3 | from scipy.integrate import solve_ivp
4 |
5 | # 1:1 binding - see https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
6 | def system01_kinetic(p, l, kdpl, interval=(0, 100)):
7 | def ode(concs, t, kdpl):
8 | p, l, pl = concs
9 | r1 = -p * l + kdpl * pl
10 | dpdt = r1
11 | dldt = r1
12 | dpldt = -r1
13 | return [dpdt, dldt, dpldt]
14 |
15 | ode_result = solve_ivp(
16 | lambda t, y: ode(y, t, kdpl), interval, [p, l, 0.0], rtol=1e-12, atol=1e-12
17 | ).y[:, -1]
18 | return {"p": ode_result[0], "l": ode_result[1], "pl": ode_result[2]}
19 |
20 |
21 | # 1:1:1 competition - see https://stevenshave.github.io/pybindingcurve/simulate_competition.html
22 | def system02_kinetic(p, l, i, kdpl, kdpi, interval=(0, 100)):
23 | def ode(concs, t, kdpl, kdpi):
24 | p, l, i, pl, pi = concs
25 | r1 = -p * l + kdpl * pl
26 | r2 = -p * i + kdpi * pi
27 | dpdt = r1 + r2
28 | dldt = r1
29 | didt = r2
30 | dpldt = -r1
31 | dpidt = -r2
32 | return [dpdt, dldt, didt, dpldt, dpidt]
33 |
34 | ode_result = solve_ivp(
35 | lambda t, y: ode(y, t, kdpl, kdpi),
36 | interval,
37 | [p, l, i, 0.0, 0.0],
38 | rtol=1e-12,
39 | atol=1e-12,
40 | ).y[:, -1]
41 | return {
42 | "p": ode_result[0],
43 | "l": ode_result[1],
44 | "i": ode_result[2],
45 | "pl": ode_result[3],
46 | "pi": ode_result[4],
47 | }
48 |
49 |
50 | # Homodimer formation - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
51 | def system03_kinetic(p, kdpp, interval=(0, 100)):
52 | def ode(concs, t, kdpp):
53 | p, pp = concs
54 | r1 = -(p * p) + kdpp * pp
55 | dpdt = 2 * r1
56 | dppdt = -r1
57 | return [dpdt, dppdt]
58 |
59 | ode_result = solve_ivp(
60 | lambda t, y: ode(y, t, kdpp), interval, [p, 0.0], rtol=1e-12, atol=1e-12
61 | ).y[:, -1]
62 | return {"p": ode_result[0], "pp": ode_result[1]}
63 |
64 |
65 | # Homodimer breaking - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
66 | def system04_kinetic(p, i, kdpp, kdpi, interval=(0, 100)):
67 | def ode(concs, t, kdpp, kdpi):
68 | p, i, pp, pi = concs
69 | r_pp = -(p * p) + kdpp * pp
70 | r_pi = -p * i + kdpi * pi
71 | dpdt = 2 * r_pp + r_pi
72 | didt = r_pi
73 | dppdt = -r_pp
74 | dpldt = -r_pi
75 | return [dpdt, didt, dppdt, didt]
76 |
77 | ode_result = solve_ivp(
78 | lambda t, y: ode(y, t, kdpp, kdpi),
79 | interval,
80 | [p, i, 0.0, 0.0],
81 | rtol=1e-12,
82 | atol=1e-12,
83 | ).y[:, -1]
84 | return {
85 | "p": ode_result[0],
86 | "i": ode_result[1],
87 | "pp": ode_result[2],
88 | "pi": ode_result[3],
89 | }
90 |
91 |
92 | class System_kinetic_one_to_one__pl(BindingSystem):
93 | """
94 | Kinetic 1:1 binding system
95 |
96 | Class defines 1:1 binding, readout is PL
97 | See https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
98 | """
99 |
100 | def __init__(self):
101 | super().__init__(system01_kinetic)
102 | self.default_readout = "pl"
103 |
104 | def query(self, parameters: dict):
105 | if self._are_ymin_ymax_present(parameters):
106 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
107 | parameters
108 | )
109 | value = super().query(parameters_no_min_max)
110 | with np.errstate(divide="ignore", invalid="ignore"):
111 | return np.nan_to_num(
112 | parameters["ymin"]
113 | + ((parameters["ymax"] - parameters["ymin"]) * value)
114 | / parameters["l"]
115 | )
116 | else:
117 | return super().query(parameters)
118 |
119 |
120 | class System_kinetic_competition_pl(BindingSystem):
121 | """
122 | Kinetic 1:1:1 competition binding system
123 |
124 | Class defines 1:1:1 competition, readout is PL
125 | See https://stevenshave.github.io/pybindingcurve/simulate_competition.html
126 | """
127 |
128 | def __init__(self):
129 | super().__init__(system02_kinetic)
130 | self.default_readout = "pl"
131 |
132 | def query(self, parameters: dict):
133 | if self._are_ymin_ymax_present(parameters):
134 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
135 | parameters
136 | )
137 | value = super().query(parameters_no_min_max)
138 | with np.errstate(divide="ignore", invalid="ignore"):
139 | return np.nan_to_num(
140 | parameters["ymin"]
141 | + ((parameters["ymax"] - parameters["ymin"]) * value)
142 | / parameters["l"]
143 | )
144 | else:
145 | return super().query(parameters)
146 |
147 |
148 | class System_kinetic_homodimerformation__pp(BindingSystem):
149 | """
150 | Kinetic homodimer formation system
151 |
152 | Class defines homodimer formation, readout is PP
153 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
154 | """
155 |
156 | def __init__(self):
157 | super().__init__(system03_kinetic, False)
158 | self.default_readout = "pp"
159 |
160 | def query(self, parameters: dict):
161 | if self._are_ymin_ymax_present(parameters):
162 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
163 | parameters
164 | )
165 | value = super().query(parameters_no_min_max)
166 | with np.errstate(divide="ignore", invalid="ignore"):
167 | return np.nan_to_num(
168 | parameters["ymin"]
169 | + ((parameters["ymax"] - parameters["ymin"]) * value)
170 | / (parameters["p"] / 2.0)
171 | )
172 | else:
173 | return super().query(parameters)
174 |
175 |
176 | class System_kinetic_homodimerbreaking__pp(BindingSystem):
177 | """
178 | Kinetic homodimer breaking system
179 |
180 | Class defines homodimer breaking, readout is PP
181 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
182 | """
183 |
184 | def __init__(self):
185 | super().__init__(system04_kinetic, False)
186 | self.default_readout = "pp"
187 |
188 | def query(self, parameters: dict):
189 | if self._are_ymin_ymax_present(parameters):
190 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
191 | parameters
192 | )
193 | value = super().query(parameters_no_min_max)
194 | with np.errstate(divide="ignore", invalid="ignore"):
195 | return np.nan_to_num(
196 | parameters["ymin"]
197 | + ((parameters["ymax"] - parameters["ymin"]) * value)
198 | / (parameters["p"] / 2.0)
199 | )
200 | else:
201 | return super().query(parameters)
202 |
203 |
204 | class System_kinetic_homodimerbreaking__pl(BindingSystem):
205 | """
206 | Kinetic homodimer breaking system
207 |
208 | Class defines homodimer breaking, readout is PL
209 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
210 | """
211 |
212 | def __init__(self):
213 | super().__init__(system04_kinetic, False)
214 | self.default_readout = "pl"
215 |
216 | def query(self, parameters: dict):
217 | if self._are_ymin_ymax_present(parameters):
218 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
219 | parameters
220 | )
221 | value = super().query(parameters_no_min_max)
222 | with np.errstate(divide="ignore", invalid="ignore"):
223 | return np.nan_to_num(
224 | parameters["ymin"]
225 | + ((parameters["ymax"] - parameters["ymin"]) * value)
226 | / (parameters["p"] / 2.0)
227 | )
228 | else:
229 | return super().query(parameters)
230 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/kinetic_systems_1_to_1to5.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from scipy.integrate import solve_ivp
3 |
4 |
5 | def system01_p_l_kd__pl(p, l, kdpl, interval=(0, 100)):
6 | def ode(concs, t, kdpl):
7 | p, l, pl = concs
8 | r1 = -p * l + kdpl * pl
9 | dpdt = r1
10 | dldt = r1
11 | dpldt = -r1
12 | return [dpdt, dldt, dpldt]
13 |
14 | ode_result = solve_ivp(
15 | lambda t, y: ode(y, t, kdpl), interval, [p, l, 0.0], rtol=1e-12, ptol=1e-12
16 | ).y[:, -1]
17 | return {"p": ode_result[0], "l": ode_result[1], "pl": ode_result[2]}
18 |
19 |
20 | def multisite_1_to_1(p, l, kd1, interval=(0, 100)):
21 | def ode_multisite_1_to_1(concs, t, kd1):
22 | p, l, p1_l = concs
23 | r1 = -p * l + kd1 * p1_l
24 | dpdt = 0.0 + r1
25 | dp1_ldt = 0.0 - r1
26 | dldt = r1
27 | return [dpdt, dldt, dp1_ldt]
28 |
29 | res = solve_ivp(
30 | lambda t, y: ode_multisite_1_to_1(y, t, kd1), interval, [p, l, 0.0]
31 | ).y[2:, -1]
32 | return (0 + 1 * res[0]) / l
33 |
34 |
35 | def multisite_1_to_2(p, l, kd1, kd2, interval=(0, 100)):
36 | def ode_multisite_1_to_2(concs, t, kd1, kd2):
37 | p, l, p1_l, p2_l, p1_2_l = concs
38 | r1 = -p * l + kd1 * p1_l
39 | r2 = -p * l + kd2 * p2_l
40 | r3 = -p1_l * l + kd2 * p1_2_l
41 | r4 = -p2_l * l + kd1 * p1_2_l
42 | dpdt = 0.0 + r1 + r2
43 | dp1_ldt = 0.0 - r1 + r3
44 | dp2_ldt = 0.0 - r2 + r4
45 | dp1_2_ldt = 0.0 - r3 - r4
46 | dldt = r1 + r2 + r3 + r4
47 | return [dpdt, dldt, dp1_ldt, dp2_ldt, dp1_2_ldt]
48 |
49 | res = solve_ivp(
50 | lambda t, y: ode_multisite_1_to_2(y, t, kd1, kd2),
51 | interval,
52 | [p, l, 0.0, 0.0, 0.0],
53 | ).y[2:, -1]
54 | return (1 * (sum(res[0:2])) + 2 * res[2]) / l
55 |
56 |
57 | def multisite_1_to_3(p, l, kd1, kd2, kd3, interval=(0, 100)):
58 | def ode_multisite_1_to_3(concs, t, kd1, kd2, kd3):
59 | p, l, p1_l, p2_l, p3_l, p1_2_l, p1_3_l, p2_3_l, p1_2_3_l = concs
60 | r1 = -p * l + kd1 * p1_l
61 | r2 = -p * l + kd2 * p2_l
62 | r3 = -p * l + kd3 * p3_l
63 | r4 = -p1_l * l + kd2 * p1_2_l
64 | r5 = -p2_l * l + kd1 * p1_2_l
65 | r6 = -p1_l * l + kd3 * p1_3_l
66 | r7 = -p3_l * l + kd1 * p1_3_l
67 | r8 = -p2_l * l + kd3 * p2_3_l
68 | r9 = -p3_l * l + kd2 * p2_3_l
69 | r10 = -p1_2_l * l + kd3 * p1_2_3_l
70 | r11 = -p1_3_l * l + kd2 * p1_2_3_l
71 | r12 = -p2_3_l * l + kd1 * p1_2_3_l
72 | dpdt = 0.0 + r1 + r2 + r3
73 | dp1_ldt = 0.0 - r1 + r4 + r6
74 | dp2_ldt = 0.0 - r2 + r5 + r8
75 | dp3_ldt = 0.0 - r3 + r7 + r9
76 | dp1_2_ldt = 0.0 - r4 - r5 + r10
77 | dp1_3_ldt = 0.0 - r6 - r7 + r11
78 | dp2_3_ldt = 0.0 - r8 - r9 + r12
79 | dp1_2_3_ldt = 0.0 - r10 - r11 - r12
80 | dldt = r1 + r2 + r3 + r4 + r5 + r6 + r7 + r8 + r9 + r10 + r11 + r12
81 | return [
82 | dpdt,
83 | dldt,
84 | dp1_ldt,
85 | dp2_ldt,
86 | dp3_ldt,
87 | dp1_2_ldt,
88 | dp1_3_ldt,
89 | dp2_3_ldt,
90 | dp1_2_3_ldt,
91 | ]
92 |
93 | res = solve_ivp(
94 | lambda t, y: ode_multisite_1_to_3(y, t, kd1, kd2, kd3),
95 | interval,
96 | [p, l, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
97 | ).y[2:, -1]
98 | return (1 * (sum(res[0:3])) + 2 * (sum(res[3:6])) + 3 * res[6]) / l
99 |
100 |
101 | def multisite_1_to_4(p, l, kd1, kd2, kd3, kd4, interval=(0, 100)):
102 | def ode_multisite_1_to_4(concs, t, kd1, kd2, kd3, kd4):
103 | (
104 | p,
105 | l,
106 | p1_l,
107 | p2_l,
108 | p3_l,
109 | p4_l,
110 | p1_2_l,
111 | p1_3_l,
112 | p1_4_l,
113 | p2_3_l,
114 | p2_4_l,
115 | p3_4_l,
116 | p1_2_3_l,
117 | p1_2_4_l,
118 | p1_3_4_l,
119 | p2_3_4_l,
120 | p1_2_3_4_l,
121 | ) = concs
122 | r1 = -p * l + kd1 * p1_l
123 | r2 = -p * l + kd2 * p2_l
124 | r3 = -p * l + kd3 * p3_l
125 | r4 = -p * l + kd4 * p4_l
126 | r5 = -p1_l * l + kd2 * p1_2_l
127 | r6 = -p2_l * l + kd1 * p1_2_l
128 | r7 = -p1_l * l + kd3 * p1_3_l
129 | r8 = -p3_l * l + kd1 * p1_3_l
130 | r9 = -p1_l * l + kd4 * p1_4_l
131 | r10 = -p4_l * l + kd1 * p1_4_l
132 | r11 = -p2_l * l + kd3 * p2_3_l
133 | r12 = -p3_l * l + kd2 * p2_3_l
134 | r13 = -p2_l * l + kd4 * p2_4_l
135 | r14 = -p4_l * l + kd2 * p2_4_l
136 | r15 = -p3_l * l + kd4 * p3_4_l
137 | r16 = -p4_l * l + kd3 * p3_4_l
138 | r17 = -p1_2_l * l + kd3 * p1_2_3_l
139 | r18 = -p1_3_l * l + kd2 * p1_2_3_l
140 | r19 = -p2_3_l * l + kd1 * p1_2_3_l
141 | r20 = -p1_2_l * l + kd4 * p1_2_4_l
142 | r21 = -p1_4_l * l + kd2 * p1_2_4_l
143 | r22 = -p2_4_l * l + kd1 * p1_2_4_l
144 | r23 = -p1_3_l * l + kd4 * p1_3_4_l
145 | r24 = -p1_4_l * l + kd3 * p1_3_4_l
146 | r25 = -p3_4_l * l + kd1 * p1_3_4_l
147 | r26 = -p2_3_l * l + kd4 * p2_3_4_l
148 | r27 = -p2_4_l * l + kd3 * p2_3_4_l
149 | r28 = -p3_4_l * l + kd2 * p2_3_4_l
150 | r29 = -p1_2_3_l * l + kd4 * p1_2_3_4_l
151 | r30 = -p1_2_4_l * l + kd3 * p1_2_3_4_l
152 | r31 = -p1_3_4_l * l + kd2 * p1_2_3_4_l
153 | r32 = -p2_3_4_l * l + kd1 * p1_2_3_4_l
154 | dpdt = 0.0 + r1 + r2 + r3 + r4
155 | dp1_ldt = 0.0 - r1 + r5 + r7 + r9
156 | dp2_ldt = 0.0 - r2 + r6 + r11 + r13
157 | dp3_ldt = 0.0 - r3 + r8 + r12 + r15
158 | dp4_ldt = 0.0 - r4 + r10 + r14 + r16
159 | dp1_2_ldt = 0.0 - r5 - r6 + r17 + r20
160 | dp1_3_ldt = 0.0 - r7 - r8 + r18 + r23
161 | dp1_4_ldt = 0.0 - r9 - r10 + r21 + r24
162 | dp2_3_ldt = 0.0 - r11 - r12 + r19 + r26
163 | dp2_4_ldt = 0.0 - r13 - r14 + r22 + r27
164 | dp3_4_ldt = 0.0 - r15 - r16 + r25 + r28
165 | dp1_2_3_ldt = 0.0 - r17 - r18 - r19 + r29
166 | dp1_2_4_ldt = 0.0 - r20 - r21 - r22 + r30
167 | dp1_3_4_ldt = 0.0 - r23 - r24 - r25 + r31
168 | dp2_3_4_ldt = 0.0 - r26 - r27 - r28 + r32
169 | dp1_2_3_4_ldt = 0.0 - r29 - r30 - r31 - r32
170 | dldt = (
171 | r1
172 | + r2
173 | + r3
174 | + r4
175 | + r5
176 | + r6
177 | + r7
178 | + r8
179 | + r9
180 | + r10
181 | + r11
182 | + r12
183 | + r13
184 | + r14
185 | + r15
186 | + r16
187 | + r17
188 | + r18
189 | + r19
190 | + r20
191 | + r21
192 | + r22
193 | + r23
194 | + r24
195 | + r25
196 | + r26
197 | + r27
198 | + r28
199 | + r29
200 | + r30
201 | + r31
202 | + r32
203 | )
204 | return [
205 | dpdt,
206 | dldt,
207 | dp1_ldt,
208 | dp2_ldt,
209 | dp3_ldt,
210 | dp4_ldt,
211 | dp1_2_ldt,
212 | dp1_3_ldt,
213 | dp1_4_ldt,
214 | dp2_3_ldt,
215 | dp2_4_ldt,
216 | dp3_4_ldt,
217 | dp1_2_3_ldt,
218 | dp1_2_4_ldt,
219 | dp1_3_4_ldt,
220 | dp2_3_4_ldt,
221 | dp1_2_3_4_ldt,
222 | ]
223 |
224 | res = solve_ivp(
225 | lambda t, y: ode_multisite_1_to_4(y, t, kd1, kd2, kd3, kd4),
226 | interval,
227 | [
228 | p,
229 | l,
230 | 0.0,
231 | 0.0,
232 | 0.0,
233 | 0.0,
234 | 0.0,
235 | 0.0,
236 | 0.0,
237 | 0.0,
238 | 0.0,
239 | 0.0,
240 | 0.0,
241 | 0.0,
242 | 0.0,
243 | 0.0,
244 | 0.0,
245 | ],
246 | ).y[2:, -1]
247 | return (
248 | 1 * (sum(res[0:4])) + 2 * (sum(res[4:10])) + 3 * (sum(res[10:14])) + 4 * res[14]
249 | ) / l
250 |
251 |
252 | def multisite_1_to_5(p, l, kd1, kd2, kd3, kd4, kd5, interval=(0, 100)):
253 | def ode_multisite_1_to_5(concs, t, kd1, kd2, kd3, kd4, kd5):
254 | (
255 | p,
256 | l,
257 | p1_l,
258 | p2_l,
259 | p3_l,
260 | p4_l,
261 | p5_l,
262 | p1_2_l,
263 | p1_3_l,
264 | p1_4_l,
265 | p1_5_l,
266 | p2_3_l,
267 | p2_4_l,
268 | p2_5_l,
269 | p3_4_l,
270 | p3_5_l,
271 | p4_5_l,
272 | p1_2_3_l,
273 | p1_2_4_l,
274 | p1_2_5_l,
275 | p1_3_4_l,
276 | p1_3_5_l,
277 | p1_4_5_l,
278 | p2_3_4_l,
279 | p2_3_5_l,
280 | p2_4_5_l,
281 | p3_4_5_l,
282 | p1_2_3_4_l,
283 | p1_2_3_5_l,
284 | p1_2_4_5_l,
285 | p1_3_4_5_l,
286 | p2_3_4_5_l,
287 | p1_2_3_4_5_l,
288 | ) = concs
289 | r1 = -p * l + kd1 * p1_l
290 | r2 = -p * l + kd2 * p2_l
291 | r3 = -p * l + kd3 * p3_l
292 | r4 = -p * l + kd4 * p4_l
293 | r5 = -p * l + kd5 * p5_l
294 | r6 = -p1_l * l + kd2 * p1_2_l
295 | r7 = -p2_l * l + kd1 * p1_2_l
296 | r8 = -p1_l * l + kd3 * p1_3_l
297 | r9 = -p3_l * l + kd1 * p1_3_l
298 | r10 = -p1_l * l + kd4 * p1_4_l
299 | r11 = -p4_l * l + kd1 * p1_4_l
300 | r12 = -p1_l * l + kd5 * p1_5_l
301 | r13 = -p5_l * l + kd1 * p1_5_l
302 | r14 = -p2_l * l + kd3 * p2_3_l
303 | r15 = -p3_l * l + kd2 * p2_3_l
304 | r16 = -p2_l * l + kd4 * p2_4_l
305 | r17 = -p4_l * l + kd2 * p2_4_l
306 | r18 = -p2_l * l + kd5 * p2_5_l
307 | r19 = -p5_l * l + kd2 * p2_5_l
308 | r20 = -p3_l * l + kd4 * p3_4_l
309 | r21 = -p4_l * l + kd3 * p3_4_l
310 | r22 = -p3_l * l + kd5 * p3_5_l
311 | r23 = -p5_l * l + kd3 * p3_5_l
312 | r24 = -p4_l * l + kd5 * p4_5_l
313 | r25 = -p5_l * l + kd4 * p4_5_l
314 | r26 = -p1_2_l * l + kd3 * p1_2_3_l
315 | r27 = -p1_3_l * l + kd2 * p1_2_3_l
316 | r28 = -p2_3_l * l + kd1 * p1_2_3_l
317 | r29 = -p1_2_l * l + kd4 * p1_2_4_l
318 | r30 = -p1_4_l * l + kd2 * p1_2_4_l
319 | r31 = -p2_4_l * l + kd1 * p1_2_4_l
320 | r32 = -p1_2_l * l + kd5 * p1_2_5_l
321 | r33 = -p1_5_l * l + kd2 * p1_2_5_l
322 | r34 = -p2_5_l * l + kd1 * p1_2_5_l
323 | r35 = -p1_3_l * l + kd4 * p1_3_4_l
324 | r36 = -p1_4_l * l + kd3 * p1_3_4_l
325 | r37 = -p3_4_l * l + kd1 * p1_3_4_l
326 | r38 = -p1_3_l * l + kd5 * p1_3_5_l
327 | r39 = -p1_5_l * l + kd3 * p1_3_5_l
328 | r40 = -p3_5_l * l + kd1 * p1_3_5_l
329 | r41 = -p1_4_l * l + kd5 * p1_4_5_l
330 | r42 = -p1_5_l * l + kd4 * p1_4_5_l
331 | r43 = -p4_5_l * l + kd1 * p1_4_5_l
332 | r44 = -p2_3_l * l + kd4 * p2_3_4_l
333 | r45 = -p2_4_l * l + kd3 * p2_3_4_l
334 | r46 = -p3_4_l * l + kd2 * p2_3_4_l
335 | r47 = -p2_3_l * l + kd5 * p2_3_5_l
336 | r48 = -p2_5_l * l + kd3 * p2_3_5_l
337 | r49 = -p3_5_l * l + kd2 * p2_3_5_l
338 | r50 = -p2_4_l * l + kd5 * p2_4_5_l
339 | r51 = -p2_5_l * l + kd4 * p2_4_5_l
340 | r52 = -p4_5_l * l + kd2 * p2_4_5_l
341 | r53 = -p3_4_l * l + kd5 * p3_4_5_l
342 | r54 = -p3_5_l * l + kd4 * p3_4_5_l
343 | r55 = -p4_5_l * l + kd3 * p3_4_5_l
344 | r56 = -p1_2_3_l * l + kd4 * p1_2_3_4_l
345 | r57 = -p1_2_4_l * l + kd3 * p1_2_3_4_l
346 | r58 = -p1_3_4_l * l + kd2 * p1_2_3_4_l
347 | r59 = -p2_3_4_l * l + kd1 * p1_2_3_4_l
348 | r60 = -p1_2_3_l * l + kd5 * p1_2_3_5_l
349 | r61 = -p1_2_5_l * l + kd3 * p1_2_3_5_l
350 | r62 = -p1_3_5_l * l + kd2 * p1_2_3_5_l
351 | r63 = -p2_3_5_l * l + kd1 * p1_2_3_5_l
352 | r64 = -p1_2_4_l * l + kd5 * p1_2_4_5_l
353 | r65 = -p1_2_5_l * l + kd4 * p1_2_4_5_l
354 | r66 = -p1_4_5_l * l + kd2 * p1_2_4_5_l
355 | r67 = -p2_4_5_l * l + kd1 * p1_2_4_5_l
356 | r68 = -p1_3_4_l * l + kd5 * p1_3_4_5_l
357 | r69 = -p1_3_5_l * l + kd4 * p1_3_4_5_l
358 | r70 = -p1_4_5_l * l + kd3 * p1_3_4_5_l
359 | r71 = -p3_4_5_l * l + kd1 * p1_3_4_5_l
360 | r72 = -p2_3_4_l * l + kd5 * p2_3_4_5_l
361 | r73 = -p2_3_5_l * l + kd4 * p2_3_4_5_l
362 | r74 = -p2_4_5_l * l + kd3 * p2_3_4_5_l
363 | r75 = -p3_4_5_l * l + kd2 * p2_3_4_5_l
364 | r76 = -p1_2_3_4_l * l + kd5 * p1_2_3_4_5_l
365 | r77 = -p1_2_3_5_l * l + kd4 * p1_2_3_4_5_l
366 | r78 = -p1_2_4_5_l * l + kd3 * p1_2_3_4_5_l
367 | r79 = -p1_3_4_5_l * l + kd2 * p1_2_3_4_5_l
368 | r80 = -p2_3_4_5_l * l + kd1 * p1_2_3_4_5_l
369 | dpdt = 0.0 + r1 + r2 + r3 + r4 + r5
370 | dp1_ldt = 0.0 - r1 + r6 + r8 + r10 + r12
371 | dp2_ldt = 0.0 - r2 + r7 + r14 + r16 + r18
372 | dp3_ldt = 0.0 - r3 + r9 + r15 + r20 + r22
373 | dp4_ldt = 0.0 - r4 + r11 + r17 + r21 + r24
374 | dp5_ldt = 0.0 - r5 + r13 + r19 + r23 + r25
375 | dp1_2_ldt = 0.0 - r6 - r7 + r26 + r29 + r32
376 | dp1_3_ldt = 0.0 - r8 - r9 + r27 + r35 + r38
377 | dp1_4_ldt = 0.0 - r10 - r11 + r30 + r36 + r41
378 | dp1_5_ldt = 0.0 - r12 - r13 + r33 + r39 + r42
379 | dp2_3_ldt = 0.0 - r14 - r15 + r28 + r44 + r47
380 | dp2_4_ldt = 0.0 - r16 - r17 + r31 + r45 + r50
381 | dp2_5_ldt = 0.0 - r18 - r19 + r34 + r48 + r51
382 | dp3_4_ldt = 0.0 - r20 - r21 + r37 + r46 + r53
383 | dp3_5_ldt = 0.0 - r22 - r23 + r40 + r49 + r54
384 | dp4_5_ldt = 0.0 - r24 - r25 + r43 + r52 + r55
385 | dp1_2_3_ldt = 0.0 - r26 - r27 - r28 + r56 + r60
386 | dp1_2_4_ldt = 0.0 - r29 - r30 - r31 + r57 + r64
387 | dp1_2_5_ldt = 0.0 - r32 - r33 - r34 + r61 + r65
388 | dp1_3_4_ldt = 0.0 - r35 - r36 - r37 + r58 + r68
389 | dp1_3_5_ldt = 0.0 - r38 - r39 - r40 + r62 + r69
390 | dp1_4_5_ldt = 0.0 - r41 - r42 - r43 + r66 + r70
391 | dp2_3_4_ldt = 0.0 - r44 - r45 - r46 + r59 + r72
392 | dp2_3_5_ldt = 0.0 - r47 - r48 - r49 + r63 + r73
393 | dp2_4_5_ldt = 0.0 - r50 - r51 - r52 + r67 + r74
394 | dp3_4_5_ldt = 0.0 - r53 - r54 - r55 + r71 + r75
395 | dp1_2_3_4_ldt = 0.0 - r56 - r57 - r58 - r59 + r76
396 | dp1_2_3_5_ldt = 0.0 - r60 - r61 - r62 - r63 + r77
397 | dp1_2_4_5_ldt = 0.0 - r64 - r65 - r66 - r67 + r78
398 | dp1_3_4_5_ldt = 0.0 - r68 - r69 - r70 - r71 + r79
399 | dp2_3_4_5_ldt = 0.0 - r72 - r73 - r74 - r75 + r80
400 | dp1_2_3_4_5_ldt = 0.0 - r76 - r77 - r78 - r79 - r80
401 | dldt = (
402 | r1
403 | + r2
404 | + r3
405 | + r4
406 | + r5
407 | + r6
408 | + r7
409 | + r8
410 | + r9
411 | + r10
412 | + r11
413 | + r12
414 | + r13
415 | + r14
416 | + r15
417 | + r16
418 | + r17
419 | + r18
420 | + r19
421 | + r20
422 | + r21
423 | + r22
424 | + r23
425 | + r24
426 | + r25
427 | + r26
428 | + r27
429 | + r28
430 | + r29
431 | + r30
432 | + r31
433 | + r32
434 | + r33
435 | + r34
436 | + r35
437 | + r36
438 | + r37
439 | + r38
440 | + r39
441 | + r40
442 | + r41
443 | + r42
444 | + r43
445 | + r44
446 | + r45
447 | + r46
448 | + r47
449 | + r48
450 | + r49
451 | + r50
452 | + r51
453 | + r52
454 | + r53
455 | + r54
456 | + r55
457 | + r56
458 | + r57
459 | + r58
460 | + r59
461 | + r60
462 | + r61
463 | + r62
464 | + r63
465 | + r64
466 | + r65
467 | + r66
468 | + r67
469 | + r68
470 | + r69
471 | + r70
472 | + r71
473 | + r72
474 | + r73
475 | + r74
476 | + r75
477 | + r76
478 | + r77
479 | + r78
480 | + r79
481 | + r80
482 | )
483 | return [
484 | dpdt,
485 | dldt,
486 | dp1_ldt,
487 | dp2_ldt,
488 | dp3_ldt,
489 | dp4_ldt,
490 | dp5_ldt,
491 | dp1_2_ldt,
492 | dp1_3_ldt,
493 | dp1_4_ldt,
494 | dp1_5_ldt,
495 | dp2_3_ldt,
496 | dp2_4_ldt,
497 | dp2_5_ldt,
498 | dp3_4_ldt,
499 | dp3_5_ldt,
500 | dp4_5_ldt,
501 | dp1_2_3_ldt,
502 | dp1_2_4_ldt,
503 | dp1_2_5_ldt,
504 | dp1_3_4_ldt,
505 | dp1_3_5_ldt,
506 | dp1_4_5_ldt,
507 | dp2_3_4_ldt,
508 | dp2_3_5_ldt,
509 | dp2_4_5_ldt,
510 | dp3_4_5_ldt,
511 | dp1_2_3_4_ldt,
512 | dp1_2_3_5_ldt,
513 | dp1_2_4_5_ldt,
514 | dp1_3_4_5_ldt,
515 | dp2_3_4_5_ldt,
516 | dp1_2_3_4_5_ldt,
517 | ]
518 |
519 | res = solve_ivp(
520 | lambda t, y: ode_multisite_1_to_5(y, t, kd1, kd2, kd3, kd4, kd5),
521 | interval,
522 | [
523 | p,
524 | l,
525 | 0.0,
526 | 0.0,
527 | 0.0,
528 | 0.0,
529 | 0.0,
530 | 0.0,
531 | 0.0,
532 | 0.0,
533 | 0.0,
534 | 0.0,
535 | 0.0,
536 | 0.0,
537 | 0.0,
538 | 0.0,
539 | 0.0,
540 | 0.0,
541 | 0.0,
542 | 0.0,
543 | 0.0,
544 | 0.0,
545 | 0.0,
546 | 0.0,
547 | 0.0,
548 | 0.0,
549 | 0.0,
550 | 0.0,
551 | 0.0,
552 | 0.0,
553 | 0.0,
554 | 0.0,
555 | 0.0,
556 | ],
557 | ).y[2:, -1]
558 | return (
559 | 1 * (sum(res[0:5]))
560 | + 2 * (sum(res[5:15]))
561 | + 3 * (sum(res[15:25]))
562 | + 4 * (sum(res[25:30]))
563 | + 5 * res[30]
564 | ) / l
565 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/lagrange_binding_system_factory.py:
--------------------------------------------------------------------------------
1 | # Warning:
2 | # Use of Lagrange-based custom systems is now DEPRECATED. Custom systems
3 | # should be generated with MinimizerBindingSystemFactory. These Lagrange-based
4 | # systems could not use MPMath for arbitary precision arithmetic, so complex
5 | # systems could become numerically unstable
6 |
7 | from inspect import signature
8 | from copy import deepcopy
9 |
10 | class LagrangeBindingSystemFactory:
11 | """
12 | LagrangeBindingSystemFactory produces custom lagrange binding fucntions
13 |
14 | From a simple definition string, such as:
15 | p+l<->pl
16 | p+i<->pi
17 |
18 | Parameters
19 | ----------
20 | bindingsystem : func
21 | A function deriving the concentration of complex formed. This fuction
22 | may be varied based on different protein-ligand binding systems.
23 | analytical: bool
24 | Perform a analytical analysis (default = False)
25 | """
26 |
27 | default_readout = None
28 | species = []
29 | fundamental_species = []
30 | reactions_dictionary = None
31 | original_reactions_dictionary = None
32 | fundamental_species_in_products = None
33 | func_string = None
34 | _add_nonzero_constraints = False
35 |
36 | def __init__(
37 | self, system_string, add_nonzero_constraints=False, output_filename=None
38 | ):
39 |
40 | # Adding non-zero constraints used in testing and development
41 | self._add_nonzero_constraints = add_nonzero_constraints
42 |
43 | # Get reactions tuples
44 | reactions = self._get_reactions_and_set_readout(system_string.lower())
45 | # Populate self.species, an ordered list of species encountered
46 | [self.species.append(s) for r in reactions for s in r if not s in self.species]
47 |
48 | # Make reactions dictionary and then simplify it
49 | self.original_reactions_dictionary = {k[2]: [] for k in reactions}
50 | [
51 | self.original_reactions_dictionary[p].append(
52 | [r1, r2, self._get_correct_kd((r1, r2, p))]
53 | )
54 | for r1, r2, p in reactions
55 | ]
56 |
57 | self.reactions_dictionary = self._simplify_reactions_dictionary(
58 | self.original_reactions_dictionary
59 | )
60 |
61 | # Find fundamental species, ordered by occurrence in systems definition string
62 | self.fundamental_species = [
63 | x
64 | for x in self.species
65 | if x in set(self.species) - set(self.reactions_dictionary.keys())
66 | ]
67 |
68 | assert (
69 | len(self.fundamental_species) > 0
70 | ), "Malformed system, no fundamental species"
71 | assert (
72 | len(self.reactions_dictionary.keys()) > 0
73 | ), "Malformed species, no products"
74 | # Find fundamental species counts in products
75 | self.fundamental_species_in_products = {}
76 | for product, counts in self._get_num_fundamental_species_by_products(
77 | self.original_reactions_dictionary
78 | ).items():
79 | for fundamental_species, count in counts.items():
80 | if (
81 | fundamental_species
82 | not in self.fundamental_species_in_products.keys()
83 | ):
84 | self.fundamental_species_in_products[fundamental_species] = {}
85 | if (
86 | product
87 | not in self.fundamental_species_in_products[
88 | fundamental_species
89 | ].keys()
90 | ):
91 | self.fundamental_species_in_products[fundamental_species][
92 | product
93 | ] = count
94 | else:
95 | self.fundamental_species_in_products[fundamental_species][
96 | product
97 | ] += count
98 |
99 | self.func_string = self.get_func_string()
100 | if output_filename is not None:
101 | self.write_func_to_python_file(output_filename)
102 | exec(self.func_string, globals())
103 | self.binding_function = eval("custom_lagrange_binding_system")
104 | self.custom_function_arguments = list(
105 | signature(self.binding_function).parameters.keys()
106 | )
107 |
108 | def write_func_to_python_file(self, filename):
109 | out_file = open(filename, "w")
110 | out_file.write(self.func_string)
111 | out_file.close()
112 |
113 | def get_func_string(self):
114 | # Write header and function definition
115 | custom_lagrange_definition = '"""\nCustom generated binding system\n\nLagrane multiplier binding system genetated with generated with \nhttps://github.com/stevenshave/lagrange-binding-systems/write_custom_system.py"""\n\n'
116 | custom_lagrange_definition += f"from scipy.optimize import fsolve\nfrom autograd import grad\ndef custom_lagrange_binding_system("
117 | custom_lagrange_definition += (
118 | ", ".join([f"{x}0" for x in self.fundamental_species])
119 | + ", "
120 | + ", ".join([x[2] for _, r in self.reactions_dictionary.items() for x in r])
121 | + "):\n"
122 | )
123 | custom_lagrange_definition += "\tdef F(X): # Augmented Lagrange function\n"
124 | # Write fundamental species concs
125 | custom_lagrange_definition += "".join(
126 | [f"\t\t{x}=X[{ix}]\n" for ix, x in enumerate(self.fundamental_species)]
127 | )
128 |
129 | # Write the mass balances
130 | mass_balances = []
131 | for k, v in self.original_reactions_dictionary.items():
132 | line = f"\t\t{k}=(" + "+".join([f"({sr[0]}*{sr[1]})" for sr in v]) + ")/("
133 | line += "+".join([sr[2] for sr in v]) + ")"
134 | mass_balances.append(line + "\n")
135 | custom_lagrange_definition += "".join(mass_balances)
136 |
137 | long_mass_balances = []
138 | for k, v in self.reactions_dictionary.items():
139 | line = f"\t\t{k}=(" + "+".join([f"({sr[0]}*{sr[1]})" for sr in v]) + ")/("
140 | line += "+".join([sr[2] for sr in v]) + ")"
141 | long_mass_balances.append(line + "\n")
142 |
143 | # Write constraints
144 | constraints = []
145 | for constraint_num in range(1, len(self.fundamental_species) + 1):
146 | line = f"\t\tconstraint{constraint_num}={self.fundamental_species[constraint_num-1]}0-({self.fundamental_species[constraint_num-1]}+"
147 | line += (
148 | "+".join(
149 | [
150 | f"{count}*{product}"
151 | for product, count in self.fundamental_species_in_products[
152 | self.fundamental_species[constraint_num - 1]
153 | ].items()
154 | ]
155 | )
156 | + ")"
157 | )
158 | constraints.append(line + "\n")
159 | if self._add_nonzero_constraints:
160 | constraints.append(
161 | f"\t\tnonzero_constraint={'-'.join([f'(constraint{x}-abs(constraint{x}))' for x in range(1, len(self.fundamental_species)+1)])}\n"
162 | )
163 | custom_lagrange_definition += "".join(constraints)
164 |
165 | # Write return statement
166 | return_statements = ["\t\treturn " + self.default_readout + "-"]
167 | return_statements.append(
168 | "+".join(
169 | [
170 | f"X[{i+len(self.fundamental_species)}]*constraint{i+1}"
171 | for i in range(len(self.fundamental_species))
172 | ]
173 | )
174 | )
175 | if self._add_nonzero_constraints:
176 | return_statements.append(
177 | f"+X[{len(self.fundamental_species)+len(self.fundamental_species)}]*nonzero_constraint"
178 | )
179 | return_statements[-1] = return_statements[-1] + "\n"
180 | custom_lagrange_definition += "".join(return_statements)
181 |
182 | # Write derivation
183 | custom_lagrange_definition += (
184 | "\tderivative_function = grad(F) # Gradients of the Lagrange function\n"
185 | )
186 |
187 | custom_lagrange_definition += (
188 | "\t"
189 | + ", ".join([f"{s}" for s in self.fundamental_species])
190 | + ", "
191 | + ", ".join(
192 | [
193 | f"lam{x}"
194 | for x in range(
195 | len(self.fundamental_species) + self._add_nonzero_constraints
196 | )
197 | ]
198 | )
199 | + " = fsolve(derivative_function, ["
200 | + ", ".join([f"{x}0" for x in self.fundamental_species])
201 | + ", "
202 | + ", ".join(
203 | [
204 | "1.0"
205 | for x in range(
206 | len(self.fundamental_species) + self._add_nonzero_constraints
207 | )
208 | ]
209 | )
210 | + "])\n"
211 | )
212 | custom_lagrange_definition += (
213 | "\treturn {"
214 | + ", ".join([f"'{s}':{s}" for s in self.fundamental_species])
215 | + ", "
216 | + ", ".join(
217 | [
218 | f"'{mb.split('=')[0].strip()}':{mb.split('=')[1].strip()}"
219 | for mb in long_mass_balances
220 | ]
221 | )
222 | + "}\n"
223 | )
224 |
225 | return custom_lagrange_definition
226 |
227 | def _get_num_fundamental_species_by_products(self, original_reactions_dict):
228 | fundamental_species_in_products = {}
229 | for product in original_reactions_dict.keys():
230 | fundamental_species_in_products[product] = {}
231 | for species in self.species:
232 | c = original_reactions_dict[product][0][0:2].count(species)
233 | if c > 0:
234 | fundamental_species_in_products[product][species] = c
235 | # Counts could contain non-fundamental products at this stage, we need to iteratively simplify.
236 | while True:
237 | something_changed = False
238 | for product, species_dict in fundamental_species_in_products.items():
239 | non_fundamental = [
240 | x for x in species_dict.keys() if x not in self.fundamental_species
241 | ]
242 | if len(non_fundamental) == 0:
243 | continue
244 | something_changed = True
245 | for nfs in non_fundamental:
246 | del species_dict[nfs]
247 | for k, v in fundamental_species_in_products[nfs].items():
248 | if k in species_dict.keys():
249 | species_dict[k] += v
250 | else:
251 | species_dict[k] = v
252 | if not something_changed:
253 | break
254 | return fundamental_species_in_products
255 |
256 | def _extract_species_numbers(self, string, species):
257 | loc = -1
258 | res = []
259 | while True:
260 | loc = string.find(species, loc + 1)
261 | if loc == -1:
262 | break
263 | i = loc + len(species)
264 | while i < len(string) and string[i].isdigit():
265 | i += 1
266 | if i == len(string):
267 | break
268 | res.append(string[loc + len(species) : i])
269 | return res
270 |
271 | def _get_correct_kd(self, reaction):
272 | """
273 | Get the correct KD for a reaction
274 |
275 | Setting the correct KD is complex when we have a system such as:
276 | P+L<->PL1, P+L<->PL2, PL1+L<->PL1L2, PL2+L<->PL1L2
277 |
278 | """
279 | r1 = reaction[0]
280 | r2 = reaction[1]
281 | p = reaction[2]
282 |
283 | # If no digits, just return the KD term
284 | if (
285 | (not (any(map(lambda x: x.isdigit(), r1))))
286 | and (not (any(map(lambda x: x.isdigit(), r2))))
287 | and (not (any(map(lambda x: x.isdigit(), p))))
288 | ):
289 | return f"kd_{r1}_{r2}"
290 |
291 | # There are digits present.
292 | species_numbers_r2_in_r1 = self._extract_species_numbers(r1, r2)
293 | species_numbers_r1_in_r2 = self._extract_species_numbers(r2, r1)
294 | species_numbers_r1_in_p = self._extract_species_numbers(p, r1)
295 | species_numbers_r2_in_p = self._extract_species_numbers(p, r2)
296 |
297 | missing_from_r1 = list(
298 | set(species_numbers_r2_in_p) - set(species_numbers_r2_in_r1)
299 | )
300 | missing_from_r2 = list(
301 | set(species_numbers_r1_in_p) - set(species_numbers_r1_in_r2)
302 | )
303 |
304 | if "" in missing_from_r1:
305 | missing_from_r1.remove("")
306 | if "" in missing_from_r2:
307 | missing_from_r2.remove("")
308 |
309 | assert (
310 | len(missing_from_r1) + len(missing_from_r2) == 1
311 | ), "A reaction should add one species to another in a clearly understandable way"
312 |
313 | if len(missing_from_r1) > 0:
314 | return f"kd_{r1}_{r2}{next(iter(missing_from_r1))}"
315 | else:
316 | return f"kd_{r1}{next(iter(missing_from_r2))}_{r2}"
317 |
318 | def _simplify_reactions_dictionary(self, reactions_dict):
319 | # Iteratively simplify the reactions dictionary until all is expressed
320 | # with fundamental species
321 | simplified_reactions_dict = deepcopy(reactions_dict)
322 | while True:
323 | something_changed = False
324 | for k, v in simplified_reactions_dict.items():
325 | for i_r, r in enumerate(v):
326 | for tuple_it in range(2):
327 | if r[tuple_it] in simplified_reactions_dict.keys():
328 | something_changed = True
329 | replacement = simplified_reactions_dict[r[tuple_it]][0]
330 | simplified_reactions_dict[k][i_r][
331 | tuple_it
332 | ] = f"{replacement[0]}*{replacement[1]}/{replacement[2]}"
333 | if not something_changed:
334 | break
335 | return simplified_reactions_dict
336 |
337 | def _get_reactions_and_set_readout(self, system_string):
338 | """
339 | Parse a system string to list of reaction tuples
340 |
341 | Gets list of reaction tuples, also sets self.readout if * is found on
342 | a species. If no * is found, readout is the product of the first
343 | reaction.
344 |
345 | """
346 |
347 | # Set up reaction_strings and tuples
348 | reaction_strings = [
349 | item.strip()
350 | for sublist in [
351 | r.split("\n") for r in system_string.split("#")[0].split(",")
352 | ]
353 | for item in sublist
354 | if len(item.strip()) > 0
355 | ]
356 | reaction_tuples = [
357 | (s[0], s[1].split("<->")[0], s[1].split("<->")[1])
358 | for s in [r.split("+") for r in reaction_strings]
359 | ]
360 |
361 | # Find the readout
362 | for i, v in enumerate(reaction_tuples):
363 | if v[0].find("*") > -1:
364 | self.default_readout = v[0].replace("*", "")
365 | reaction_tuples[i] = (v[0].replace("*", ""), v[1], v[2])
366 | if v[1].find("*") > -1:
367 | self.default_readout = v[1].replace("*", "")
368 | reaction_tuples[i] = (v[0], v[1].replace("*", ""), v[2])
369 | if v[2].find("*") > -1:
370 | self.default_readout = v[2].replace("*", "")
371 | reaction_tuples[i] = (v[0], v[1], v[2].replace("*", ""))
372 | if not self.default_readout:
373 | self.default_readout = reaction_tuples[0][2]
374 |
375 | return reaction_tuples
376 |
377 |
378 | if __name__ == "__main__":
379 | custom_system = """
380 | P+P<->PP*
381 | P+L<->PL
382 | PP+L<->PPL1
383 | PP+L<->PPL2
384 | PPL1+L<->PPL1L2
385 | PPL2+L<->PPL1L2
386 | """
387 | new_lagrange = LagrangeBindingSystemFactory(custom_system)
388 | print(f"{new_lagrange.custom_function_arguments}")
389 | print(new_lagrange.binding_function(10, 10, 55, 10, 10, 10, 10, 10))
390 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/lagrange_systems.py:
--------------------------------------------------------------------------------
1 | from scipy.optimize import fsolve
2 | from autograd import grad
3 | from .binding_system import BindingSystem
4 | from .lagrange_binding_system_factory import LagrangeBindingSystemFactory
5 | import numpy as np
6 |
7 |
8 | # 1:1 binding - see https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
9 | def system01_lagrange(p, l, kdpl):
10 | def F(X): # Augmented Lagrange function
11 | response = (X[0] * X[1]) / kdpl
12 | return (
13 | response - X[2] * (p - (response + X[0])) - X[3] * (l - (response + X[1]))
14 | )
15 |
16 | dfdL = grad(F) # Gradients of the Lagrange function
17 | pf, lf, lam1, lam2 = fsolve(dfdL, [p, l, 1.0, 1.0])
18 | return {"pf": pf, "lf": lf, "pl": (pf * lf) / kdpl}
19 |
20 |
21 | # 1:1:1 competition - see https://stevenshave.github.io/pybindingcurve/simulate_competition.html
22 | def system02_lagrange(p, l, i, kdpl, kdpi):
23 | def F(X): # Augmented Lagrange function
24 | response = (X[0] * X[1]) / kdpl
25 | constraint1 = p - (response + X[0] + (X[0] * X[2] / kdpi))
26 | constraint2 = l - (response + X[1])
27 | constraint3 = i - (X[2] + (X[0] * X[2] / kdpi))
28 | return response - X[3] * constraint1 - X[4] * constraint2 - X[5] * constraint3
29 |
30 | dfdL = grad(F, 0) # Gradients of the Lagrange function
31 | pf, lf, inhf, lam1, lam2, lam3 = fsolve(dfdL, [p, l, i, 1.0, 1.0, 1.0])
32 | return {"pf": pf, "lf": lf, "pl": (pf * lf) / kdpl}
33 |
34 |
35 | # Homodimer formation - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
36 | def system03_lagrange(p, kdpp):
37 | def F(X): # Augmented Lagrange function
38 | response = (X[0] * X[0]) / kdpp
39 | constraint1 = p - (response * 2 + X[0])
40 | return response - X[1] * constraint1
41 |
42 | dfdL = grad(F, 0) # Gradients of the Lagrange function
43 | pf, lam1 = fsolve(dfdL, [p, 1.0])
44 | return {"pf": pf, "pp": ((pf * pf) / kdpp)}
45 |
46 |
47 | # Homodimer breaking - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
48 | def system04_lagrange(p, i, kdpp, kdpi):
49 | def F(X): # Augmented Lagrange function
50 | pp = (X[0] * X[0]) / kdpp
51 | pi = (X[0] * X[1]) / kdpi
52 | constraint1 = p - (pp * 2 + pi + X[0])
53 | constraint2 = i - (pi + X[1])
54 | return pp - X[2] * constraint1 - X[3] * constraint2
55 |
56 | dfdL = grad(F, 0) # Gradients of the Lagrange function
57 | pf, i_f, lam1, lam2 = fsolve(dfdL, [p, i, 1.0, 1.0])
58 | return {"pf": pf, "if": i_f, "pp": ((pf * pf) / kdpp), "pi": (pf * i_f) / kdpi}
59 |
60 |
61 | # 1:2 binding
62 | def system12_lagrange(p, l, kdpl1, kdpl2):
63 | def F(X): # Augmented Lagrange function
64 | pf = X[0]
65 | lf = X[1]
66 | pl1 = pf * lf / kdpl1
67 | pl2 = pf * lf / kdpl2
68 | pl12 = (pl1 * lf + pl2 * lf) / (kdpl1 + kdpl2)
69 | constraint1 = p - (pf + pl1 + pl2 + pl12)
70 | constraint2 = l - (lf + pl1 + pl2 + 2 * pl12)
71 | return pl12 - X[2] * constraint1 - X[3] * constraint2
72 |
73 | dfdL = grad(F, 0) # Gradients of the Lagrange function
74 |
75 | pf, lf, lam1, lam2 = fsolve(dfdL, [p, l] + [1.0] * 2)
76 | pl1 = pf * lf / kdpl1
77 | pl2 = pf * lf / kdpl2
78 | pl12 = (pl1 * lf + pl2 * lf) / (kdpl1 + kdpl2)
79 | return {"pf": pf, "lf": lf, "pl1": pl1, "pl2": pl2, "pl12": pl12}
80 |
81 |
82 | # 1:3 binding
83 | def system13_lagrange(p, l, kdpl1, kdpl2, kdpl3):
84 | def F(X): # Augmented Lagrange function
85 | pf = X[0]
86 | lf = X[1]
87 | pl1 = pf * lf / kdpl1
88 | pl2 = pf * lf / kdpl2
89 | pl3 = pf * lf / kdpl3
90 | pl12 = (pl1 * lf + pl2 * lf) / (kdpl1 + kdpl2)
91 | pl13 = (pl1 * lf + pl3 * lf) / (kdpl1 + kdpl3)
92 | pl23 = (pl2 * lf + pl3 * lf) / (kdpl2 + kdpl3)
93 | pl123 = (pl12 * lf + pl13 * lf + pl23 * lf) / (kdpl1 + kdpl2 + kdpl3)
94 | constraint1 = p - (pf + pl1 + pl2 + pl3 + pl12 + pl13 + pl23 + pl123)
95 | constraint2 = l - (
96 | lf + pl1 + pl2 + pl3 + 2 * (pl12 + pl13 + pl23) + 3 * (pl123)
97 | )
98 | nonzero_constraint = (constraint1 - abs(constraint1)) - (
99 | constraint2 - abs(constraint2)
100 | )
101 | return (
102 | pl123 - X[2] * constraint1 - X[3] * constraint2 - X[4] * nonzero_constraint
103 | )
104 |
105 | dfdL = grad(F, 0) # Gradients of the Lagrange function
106 | pf, lf, lam1, lam2, lam3 = fsolve(dfdL, [p, l] + [1.0] * 3)
107 | pl1 = pf * lf / kdpl1
108 | pl2 = pf * lf / kdpl2
109 | pl3 = pf * lf / kdpl3
110 | pl12 = (pl1 * lf + pl2 * lf) / (kdpl1 + kdpl2)
111 | pl13 = (pl1 * lf + pl3 * lf) / (kdpl1 + kdpl3)
112 | pl23 = (pl2 * lf + pl3 * lf) / (kdpl2 + kdpl3)
113 | pl123 = (pl12 * lf + pl13 * lf + pl23 * lf) / (kdpl1 + kdpl2 + kdpl3)
114 | return {
115 | "pf": pf,
116 | "lf": lf,
117 | "pl1": pl1,
118 | "pl2": pl2,
119 | "pl3": pl3,
120 | "pl12": pl12,
121 | "pl13": pl13,
122 | "pl23": pl23,
123 | "pl123": pl123,
124 | }
125 |
126 |
127 | class System_lagrange_one_to_one__pl(BindingSystem):
128 | """
129 | Lagrange 1:1 binding system
130 |
131 | Class defines 1:1 binding, readout is PL
132 | See https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
133 | """
134 |
135 | def __init__(self):
136 | super().__init__(system01_lagrange)
137 | self.default_readout = "pl"
138 |
139 | def query(self, parameters: dict):
140 | if self._are_ymin_ymax_present(parameters):
141 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
142 | parameters
143 | )
144 | value = super().query(parameters_no_min_max)
145 | with np.errstate(divide="ignore", invalid="ignore"):
146 | return np.nan_to_num(
147 | parameters["ymin"]
148 | + ((parameters["ymax"] - parameters["ymin"]) * value)
149 | / parameters["l"]
150 | )
151 | else:
152 | return super().query(parameters)
153 |
154 |
155 | class System_lagrange_competition_pl(BindingSystem):
156 | """
157 | Lagrange 1:1:1 competition binding system
158 |
159 | Class defines 1:1:1 competition, readout is PL
160 | See https://stevenshave.github.io/pybindingcurve/simulate_competition.html
161 | """
162 |
163 | def __init__(self):
164 | super().__init__(system02_lagrange)
165 | self.default_readout = "pl"
166 |
167 | def query(self, parameters: dict):
168 | if self._are_ymin_ymax_present(parameters):
169 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
170 | parameters
171 | )
172 | value = super().query(parameters_no_min_max)
173 | with np.errstate(divide="ignore", invalid="ignore"):
174 | return np.nan_to_num(
175 | parameters["ymin"]
176 | + ((parameters["ymax"] - parameters["ymin"]) * value)
177 | / parameters["l"]
178 | )
179 | else:
180 | return super().query(parameters)
181 |
182 |
183 | class System_lagrange_homodimerformation__pp(BindingSystem):
184 | """
185 | Lagrange homodimer formation system
186 |
187 | Class defines homodimer formation, readout is PP
188 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
189 | """
190 |
191 | def __init__(self):
192 | super().__init__(system03_lagrange, False)
193 | self.default_readout = "pp"
194 |
195 | def query(self, parameters: dict):
196 | if self._are_ymin_ymax_present(parameters):
197 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
198 | parameters
199 | )
200 | value = super().query(parameters_no_min_max)
201 | with np.errstate(divide="ignore", invalid="ignore"):
202 | return np.nan_to_num(
203 | parameters["ymin"]
204 | + ((parameters["ymax"] - parameters["ymin"]) * value)
205 | / (parameters["p"] / 2.0)
206 | )
207 | else:
208 | return super().query(parameters)
209 |
210 |
211 | class System_lagrange_homodimerbreaking__pp(BindingSystem):
212 | """
213 | Lagrange homodimer breaking system
214 |
215 | Class defines homodimer breaking, readout is PP
216 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
217 | """
218 |
219 | def __init__(self):
220 | super().__init__(system04_lagrange, False)
221 | self.default_readout = "pp"
222 |
223 | def query(self, parameters: dict):
224 | if self._are_ymin_ymax_present(parameters):
225 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
226 | parameters
227 | )
228 | value = super().query(parameters_no_min_max)
229 | with np.errstate(divide="ignore", invalid="ignore"):
230 | return np.nan_to_num(
231 | parameters["ymin"]
232 | + ((parameters["ymax"] - parameters["ymin"]) * value)
233 | / (parameters["p"] / 2.0)
234 | )
235 | else:
236 | return super().query(parameters)
237 |
238 |
239 | class System_lagrange_homodimerbreaking__pl(BindingSystem):
240 | """
241 | Lagrange homodimer breaking system
242 |
243 | Class defines homodimer breaking, readout is PL
244 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
245 | """
246 |
247 | def __init__(self):
248 | super().__init__(system04_lagrange, False)
249 | self.default_readout = "pl"
250 |
251 | def query(self, parameters: dict):
252 | if self._are_ymin_ymax_present(parameters):
253 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
254 | parameters
255 | )
256 | value = super().query(parameters_no_min_max)
257 | with np.errstate(divide="ignore", invalid="ignore"):
258 | return np.nan_to_num(
259 | parameters["ymin"]
260 | + ((parameters["ymax"] - parameters["ymin"]) * value)
261 | / (parameters["p"] / 2.0)
262 | )
263 | else:
264 | return super().query(parameters)
265 |
266 |
267 | class System_lagrange_1_to_2__pl12(BindingSystem):
268 | """
269 | Lagrange 1:2 binding system.
270 |
271 | Class defines 1:2 protein:ligand binding, readout is PL12, meaning protein
272 | with 2 ligands
273 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
274 | """
275 |
276 | def __init__(self):
277 | super().__init__(system12_lagrange, False)
278 | self.default_readout = "pl12"
279 |
280 | def query(self, parameters: dict):
281 | if self._are_ymin_ymax_present(parameters):
282 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
283 | parameters
284 | )
285 | value = super().query(parameters_no_min_max)
286 | with np.errstate(divide="ignore", invalid="ignore"):
287 | return np.nan_to_num(
288 | parameters["ymin"]
289 | + ((parameters["ymax"] - parameters["ymin"]) * value)
290 | / parameters["l"]
291 | )
292 | else:
293 | return super().query(parameters)
294 |
295 |
296 | class System_lagrange_1_to_3__pl123(BindingSystem):
297 | """
298 | Lagrange 1:2 binding system.
299 |
300 | Class defines 1:2 protein:ligand binding, readout is PL123, meaning protein
301 | with 3 ligands
302 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
303 | """
304 |
305 | def __init__(self):
306 | super().__init__(system13_lagrange, False)
307 | self.default_readout = "pl123"
308 |
309 | def query(self, parameters: dict):
310 | if self._are_ymin_ymax_present(parameters):
311 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
312 | parameters
313 | )
314 | value = super().query(parameters_no_min_max)
315 | with np.errstate(divide="ignore", invalid="ignore"):
316 | return np.nan_to_num(
317 | parameters["ymin"]
318 | + ((parameters["ymax"] - parameters["ymin"]) * value)
319 | / parameters["l"]
320 | )
321 | else:
322 | return super().query(parameters)
323 |
324 |
325 | class System_lagrange_custom(BindingSystem):
326 | """
327 | Lagrange custom binding system
328 |
329 | Class uses LagrangeBindingSystemFactory to make a custom lagrange function.
330 | """
331 |
332 | def __init__(self, system_string):
333 | custom_system = LagrangeBindingSystemFactory(system_string)
334 | super().__init__(custom_system.binding_function)
335 | self.default_readout = custom_system.default_readout
336 |
337 | def query(self, parameters: dict):
338 | if self._are_ymin_ymax_present(parameters):
339 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
340 | parameters
341 | )
342 | value = super().query(parameters_no_min_max)
343 | with np.errstate(divide="ignore", invalid="ignore"):
344 | return np.nan_to_num(
345 | parameters["ymin"]
346 | + ((parameters["ymax"] - parameters["ymin"]) * value)
347 | / parameters["l"]
348 | )
349 | else:
350 | return super().query(parameters)
351 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/minimizer_binding_system_factory.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | import numpy as np
3 | from sys import version_info
4 | from mpmath import mpf, mp, findroot, almosteq
5 |
6 | class MinimizerBindingSystemFactory:
7 | """
8 | MinimizerBindingSystemFactory produces custom minimizer-based binding
9 | system fucntions
10 |
11 | From a simple definition string representing standard 1:1:1 competition, such as in Example 1:
12 |
13 | Example 1:
14 | ----------
15 | p+l<->pl
16 | p+i<->pi
17 |
18 | To more complex examples such as example 2:
19 |
20 | Example 2:
21 | ----------
22 | P+P<->PP*
23 | P+L<->PL
24 | PP+L<->PPL1
25 | PP+L<->PPL2
26 | PPL1+L<->PPL1L2
27 | PPL2+L<->PPL1L2
28 |
29 | Example 2 should produce the following function - which is annotated to indicate
30 | sections which this class builds through parsing of the system definition string.
31 | Numbers like ### 1 ### denote sections which are described bellow and documented
32 | in the code
33 |
34 | def custom_minimizer_system(p,l,kd_p_p_pp,kd_p_l_pl,kd_pp_l_ppl1,kd_pp_l_ppl2,kd_ppl1_l_ppl1l2,kd_ppl2_l_ppl1l2): ### 1 ###
35 | p=mpf(p) ### 2 ###
36 | l=mpf(l) ### 2 ###
37 | kd_p_p_pp=mpf(kd_p_p_pp) ### 3 ###
38 | kd_p_l_pl=mpf(kd_p_l_pl) ### 3 ###
39 | kd_pp_l_ppl1=mpf(kd_pp_l_ppl1) ### 3 ###
40 | kd_pp_l_ppl2=mpf(kd_pp_l_ppl2) ### 3 ###
41 | kd_ppl1_l_ppl1l2=mpf(kd_ppl1_l_ppl1l2) ### 3 ###
42 | kd_ppl2_l_ppl1l2=mpf(kd_ppl2_l_ppl1l2) ### 3 ###
43 | def f(p_f,l_f): ### 4 ###
44 | pp=p_f*p_f/kd_p_p_pp ### 5 ###
45 | pl=p_f*l_f/kd_p_l_pl ### 5 ###
46 | ppl1=pp*l_f/kd_pp_l_ppl1 ### 5 ###
47 | ppl2=pp*l_f/kd_pp_l_ppl2 ### 5 ###
48 | ppl1l2=(ppl1*l_f+ppl2*l_f)/(kd_ppl1_l_ppl1l2+kd_ppl2_l_ppl1l2) ### 5 ###
49 | return p0-(p+2*pp+2*ppl1+2*ppl2+pl+2*ppl1l2),l0-(l+ppl1+ppl2+pl+2*ppl1l2) ### 6 ###
50 | p_f,l_f=findroot(f, [mpf(0), mpf(0)], tol=1e-10) ### 7 ###
51 | pp=p_f*p_f/kd_p_p_pp ### 5 ###
52 | pl=p_f*l_f/kd_p_l_pl ### 5 ###
53 | ppl1=pp*l_f/kd_pp_l_ppl1 ### 5 ###
54 | ppl2=pp*l_f/kd_pp_l_ppl2 ### 5 ###
55 | ppl1l2=(ppl1*l_f+ppl2*l_f)/(kd_ppl1_l_ppl1l2+kd_ppl2_l_ppl1l2) ### 5 ###
56 | return {'p_f':p_f,'l_f':l_f,'pp':pp,'pl':pl,'ppl1':ppl1,'ppl2':ppl2,'ppl1l2':ppl1l2} ### 8 ###
57 |
58 | Section ### 1 ### : Define the custom minimizer function which takes arguments for fundamental species
59 | centration and KDs.
60 | Section ### 2 ### : Cast input fundamental species to mpf arbitary precision datatypes.
61 | Section ### 3 ### : Cast input KDs to mpf arbitary precision datatypes.
62 | Section ### 4 ### : Define objective function for the minimiser.
63 | Section ### 5 ### : Calculate species concentrations from fundamental, and other dependant species.
64 | Section ### 6 ### : Return tuple containing the deviation from total fundamental species concentrations.
65 | More difficult than it first appears as we must count the number of fundamental
66 | species monomers in each species to know what to multiply the concentration by.
67 | Section ### 7 ### : Run the mpmath find_root function on the newly defined objective function, minimising
68 | the values in returned tuples. This reflects the fundamental species free concentration
69 | at equilibrium.
70 | Section ### 8 ### : Return the dictionary of results, containing concentrations for all species at
71 | equilibrium.
72 |
73 | """
74 |
75 | assert version_info >= (3, 7), "Requires Python version >=3.7 as dictionary insertion order need to be preserved"
76 | # Readout denotes the species abundance read out when constructing a BindingSystem
77 | # object for use in PyBindingCurve simulations/fitting.
78 | readout = None
79 |
80 | # When generated, the custom function string, the custom function, and its
81 | # arguments are stored as member variables.
82 | binding_function_string = None
83 | binding_function=None
84 | binding_function_arguments=None
85 |
86 | def __init__(self, system_string:str, output_filename=None, dps:int=100):
87 | """Construct a minimiser-based custom binding system object
88 |
89 | Args:
90 | system_string (str): Custom system definition string
91 | output_filename ([str, Path], optional): Optional file to write generated function to. Defaults to None.
92 | dps (int, optional): Decimal precision used for calculations performed by MPMath. Defaults to 100.
93 | """
94 | # Get the reaction dictionary, which takes the form:
95 | # reaction_dictionary[product]=[[reactant1, reactant2]]
96 | # list may contain multiple approaches to make product.
97 | reaction_dict = self.parse_system_definition_string(system_string)
98 |
99 | # Now find species (all species present), and fundamental_species
100 | # dictionaries. They are dictionaries as python >= 3.7 guarantees
101 | # dicts are ordered. They are used here essentailly like ordered
102 | # sets, with keys as values for species and values = None.
103 | species, fundamental_species = self.get_species_and_fundamental_species(reaction_dict)
104 |
105 | # Species_composed_of_matrix is an np array of
106 | # shape=(len(fundamental_species), len(species)). Rows and columns
107 | # ordered by appearance in species and fundamental_species dicts.
108 | # See function docstring for more complete definition and example.
109 | species_composed_of_matrix = self.build_species_composed_of_matrix(species, fundamental_species, reaction_dict)
110 |
111 | # Get kds needed, in order of appearance in reaction_dict. Again, dict
112 | # is used to gain functionality of an ordered set.
113 | kds = self.get_kds(reaction_dict)
114 |
115 | # In cases where there are two routes to make a product, with r1 and
116 | # r2, or by r3 and r4, the amount must be calculated in a special way:
117 | # product = r1*r2+r3*r4/(kd1+kd2).
118 | # Simplified reaction_dict associates one equation with one reaction
119 | # product for writing out.
120 | simplified_reaction_dict = self.get_simplified_reaction_dict(reaction_dict, species, fundamental_species)
121 |
122 | # Generate the function string and store in self.custom_func_string
123 | self.binding_function_string = self.gen_custom_func(species,fundamental_species,kds,simplified_reaction_dict,species_composed_of_matrix)
124 |
125 | # If requested, write the function to a file
126 | if output_filename is not None:
127 | out_file = open(output_filename, "w")
128 | out_file.write("from mpmath import mpf, findroot, mp, almosteq\nmp.dps=100\n\n")
129 | out_file.write(self.binding_function_string)
130 | out_file.close()
131 |
132 | exec(self.binding_function_string, globals())
133 | self.binding_function = eval("custom_minimizer_system")
134 | self.binding_function_arguments = list(signature(self.binding_function).parameters.keys())
135 |
136 |
137 | def get_simplified_reaction_dict(self, reaction_dict, species, fundamental_species):
138 | """Get simplified reaction dict, required when a product can be made in more than one way
139 |
140 | Args:
141 | reaction_dict (dict): Reaction dictionary derived from custom binding system string
142 | species (dict): Dict used as ordered set, containing all species
143 | fundamental_species (dict): Dict used as ordered set, containing only fundamental species
144 |
145 | Returns:
146 | dict: Simplified reaction dict, keys are products, values are equations to calculate amounts
147 | """
148 | srd = {}
149 | for product, reactions in reaction_dict.items():
150 | # Only 1 way to make the product (simple case)
151 | if len(reactions) == 1:
152 | r1 = reactions[0][0]
153 | r2 = reactions[0][1]
154 | if r1 in fundamental_species.keys():
155 | r1 = f"{r1}_f"
156 | if r2 in fundamental_species.keys():
157 | r2 = f"{r2}_f"
158 | srd[
159 | product
160 | ] = f"{r1}*{r2}/{self.kd_from_reaction_tuple(reactions[0],product)}"
161 | else:
162 | # 2 or more ways to make the product
163 | top = ""
164 | bottom = ""
165 | for reaction in reactions:
166 | r1 = reaction[0]
167 | r2 = reaction[1]
168 | if r1 in fundamental_species.keys():
169 | r1 = f"{r1}_f"
170 | if r2 in fundamental_species.keys():
171 | r2 = f"{r2}_f"
172 | top += f"{r1}*{r2}+"
173 | bottom += f"{self.kd_from_reaction_tuple(reaction,product)}+"
174 | top = top[:-1]
175 | bottom = bottom[:-1]
176 | srd[product] = f"({top})/({bottom})"
177 | return srd
178 |
179 | def gen_custom_func(self, species: dict, fundamental_species: dict, kds: dict, simplified_reaction_dict: dict, species_composed_of_matrix: np.array):
180 | """Generate custom function text
181 |
182 | Args:
183 | species (dict): All species in system
184 | fundamental_species (dict): Fundamental species in system
185 | kds (dict): Unique KDs in order of appearance in the system.
186 | simplified_reaction_dict (dict): Simplified/unified reaction dictionary
187 | species_composed_of_matrix (np.array): Numpy int array of monomer counts for all species with shape (len(fundamental_species), len(species))
188 |
189 | Returns:
190 | str: String representing the custom binding system, solved using MPMath findroot
191 |
192 | The docstring for the MinimizerBindingSystemFactory class outlines the target function text which this function generates.
193 | Areas denoted by ### 1 ### documented in that text are marked in comments bellow, showing how function construction proceeds.
194 | """
195 |
196 | # Section ### 1 ### : Define the custom minimizer function which takes
197 | # arguments for fundamental species centration and KDs.
198 | text = (
199 | f"def custom_minimizer_system("
200 | + ",".join(f for f in fundamental_species)
201 | + ","
202 | + ",".join(kd for kd in kds)
203 | + "):\n"
204 | )
205 |
206 | # Section ### 2 ### : Cast input fundamental species to mpf arbitary
207 | # precision datatypes.
208 | for fs in fundamental_species:
209 | text += f"\t{fs}=mpf({fs})\n"
210 |
211 | # Section ### 3 ### : Cast input KDs to mpf arbitary precision
212 | # datatypes.
213 | for kd in kds:
214 | text += f"\t{kd}=mpf({kd})\n"
215 | text +=f"\tif almosteq({kd}, mpf(0), mpf(\"1e-10\")):\n"
216 | text +=f"\t\t{kd}+=mpf(\"1e-10\")\n"
217 |
218 |
219 | # Section ### 4 ### : Define objective function for the minimiser.
220 | text += "\tdef f(" + ",".join(f"{fs}_f" for fs in fundamental_species) + "):\n"
221 |
222 | # Section ### 5 ### : Calculate species concentrations from
223 | # fundamental, and other dependant species.
224 | for product, reaction in simplified_reaction_dict.items():
225 | text += f"\t\t{product}={reaction}\n"
226 |
227 | # Section ### 6 ### : Return tuple containing the deviation from total
228 | # fundamental species concentrations. More difficult than it first
229 | # appears as we must count the number of fundamental species monomers
230 | # in each species to know what to multiply the concentration by.
231 | fundamentals_zero_sum_strs = []
232 | for fsi, fs in enumerate(fundamental_species):
233 | monomer_counts = species_composed_of_matrix[:, fsi]
234 | balance_str = f"{fs}-"
235 | to_consider = []
236 | for mci, count in enumerate(monomer_counts):
237 | if count == 0:
238 | continue
239 | monomer_name=list(species.keys())[mci]
240 | if monomer_name in fundamental_species:
241 | monomer_name=monomer_name+"_f"
242 | if count == 1:
243 | to_consider.append(f"{monomer_name}")
244 | else:
245 | to_consider.append(f"{count}*{monomer_name}")
246 | balance_str += f"({'+'.join(to_consider)})"
247 | fundamentals_zero_sum_strs.append(balance_str)
248 | text += "\t\treturn " + ",".join(fundamentals_zero_sum_strs) + "\n"
249 |
250 | # Section ### 7 ### : Run the mpmath find_root function on the newly
251 | # defined objective function, minimising the values in returned
252 | # tuples. This reflects the fundamental species free concentration
253 | # at equilibrium.
254 | text += (
255 | "\t"
256 | + ",".join(f"{fs}_f" for fs in fundamental_species)
257 | + "=findroot(f, ["
258 | + ", ".join(f"mpf(0)" for fs in fundamental_species)
259 | + "], tol=1e-10, maxsteps=1e6)\n"
260 | )
261 |
262 | # Section ### 5 ### REPEATED: Calculate species concentrations from
263 | # fundamental, and other dependant species, same as previously written
264 | # out for objective function
265 | for product, reaction in simplified_reaction_dict.items():
266 | text += f"\t{product}={reaction}\n"
267 |
268 | # Section ### 8 ### : Return the dictionary of results, containing
269 | # concentrations for all species at equilibrium.
270 | text += "\treturn {"
271 | for fs in fundamental_species:
272 | text += f"'{fs}_f':{fs}_f,"
273 | for product in simplified_reaction_dict:
274 | text += f"'{product}':{product},"
275 | text = text[:-1] + "}\n"
276 |
277 | return text
278 |
279 | def get_kds(self, reaction_dictionary: dict):
280 | """Get KDs dictionary from reaction_dictionary
281 |
282 | Args:
283 | reaction_dictionary (dict): Reaction dictionary
284 |
285 | Returns:
286 | dict: Dict of KDs written properly. Dict is used as it mimics an ordered set
287 | """
288 | kds = {}
289 | for product, reactions in reaction_dictionary.items():
290 | for reaction in reactions:
291 | kds[self.kd_from_reaction_tuple(reaction, product)] = None
292 | return kds
293 |
294 | def kd_from_reaction_tuple(self, reaction_tuple: tuple, product: str):
295 | """Convenience function to turn (r1, r2), product into kd_r1_r2_product
296 |
297 | Args:
298 | reaction_tuple (tuple): Reaction tuple, like ('r1', 'r2')
299 | product (str): product name/symbol
300 |
301 | Returns:
302 | str: String of the form 'kd_r1_r2_product'
303 | """
304 | return f"kd_{reaction_tuple[0]}_{reaction_tuple[1]}_{product}"
305 |
306 | def parse_system_definition_string(self, system_string: str):
307 | """Parse system definition, generate reaction_dict
308 |
309 | Args:
310 | system_string (str): Custom system definition
311 | Returns:
312 | dict: reaction dictionary product:(reactant1, reactant2)
313 | """
314 | reaction_dict = {}
315 |
316 | lines = [
317 | l for l in [c.strip() for nl in system_string.lower().split("\n") for c in nl.split(",") if len(c.strip())>=7]
318 | ]
319 | assert len(lines) > 0, "No system defined"
320 |
321 | # Loop over lines parsing reactions, setting readout, and generating reaction_dict
322 | for line in lines:
323 | plus_loc = line.find("+")
324 | bracket_loc = line.find(">")
325 | reactant1 = line[0:plus_loc]
326 | reactant2 = line[plus_loc + 1 : bracket_loc - 2]
327 | product = line[bracket_loc + 1 :]
328 |
329 | # Check for stars to indicate desired readout
330 | if "*" in reactant1:
331 | reactant1 = reactant1.replace("*", "")
332 | self.readout = reactant1
333 | if "*" in reactant2:
334 | reactant2 = reactant2.replace("*", "")
335 | self.readout = reactant2
336 | if "*" in product:
337 | product = product.replace("*", "")
338 | self.readout = product
339 |
340 | # Add to the objects reaction_dict
341 | if product not in reaction_dict.keys():
342 | reaction_dict[product] = []
343 | reaction_dict[product].append([reactant1, reactant2])
344 |
345 | # Set the readout.
346 | if self.readout is None:
347 | first_product = list(reaction_dict.keys())[0]
348 | print(
349 | f"* character not found to set readout in custom system, using the first product, which is: {first_product}"
350 | )
351 | self.readout = first_product
352 |
353 | return reaction_dict
354 |
355 | def build_species_composed_of_matrix(self, species: dict, fundamental_species: dict, reaction_dict: dict):
356 | """Make matrix of fund_species x species containing monomer occurence counts for each species
357 |
358 | Args:
359 | species (dict): All species dict
360 | fundamental_species (dict): Fundamental species dict
361 | reaction_dict (dict): Reaction dictionary
362 |
363 | Returns:
364 | np.array: Monomer counts array, shape=(len(fundamental_species), len(species))
365 |
366 | # Species_composed_of_matrix is an np array of
367 | # shape=(len(fundamental_species), len(species)). Rows and columns
368 | # ordered by appearance in species and fundamental_species dicts.
369 | # The following system:
370 | # p+p<->pp
371 | # pp+l<->ppl
372 | # would produce a species_composed_of_matrix as follows:
373 | # P L
374 | # - -
375 | # P |1 0
376 | # L |0 1
377 | # PP |2 0
378 | # PPL|2 1
379 | """
380 | len_species = len(species)
381 | len_fundamental_species = len(fundamental_species)
382 | species_composed_of_matrix = np.zeros(
383 | (len_species, len_fundamental_species), dtype=int
384 | )
385 | for fsi, _ in enumerate(fundamental_species):
386 | species_composed_of_matrix[fsi, fsi] = 1
387 |
388 | for si, s in enumerate(list(species.keys())[len_fundamental_species:]):
389 | species_composed_of_matrix[len_fundamental_species + si, :] = (
390 | species_composed_of_matrix[
391 | list(species.keys()).index(reaction_dict[s][0][0]), :
392 | ]
393 | + species_composed_of_matrix[
394 | list(species.keys()).index(reaction_dict[s][0][1]), :
395 | ]
396 | )
397 | return species_composed_of_matrix
398 |
399 | def get_species_and_fundamental_species(self, reaction_dict: dict):
400 | """Get species and fundamental species dictionaries
401 |
402 | Args:
403 | reaction_dict (dict): Reaction dictionary
404 |
405 | Returns:
406 | tuple(dict, dict): Species and fundamental_species dictonaries
407 | """
408 | species = {}
409 | fundamental_species = {}
410 | # Populate species
411 | for product, reactions in reaction_dict.items():
412 | for reactant1, reactant2 in reactions:
413 | species[reactant1] = None
414 | species[reactant2] = None
415 | for product in reaction_dict:
416 | species[product] = None
417 |
418 | # Populate fundamental_species
419 | for s in species.keys():
420 | if s not in reaction_dict.keys():
421 | fundamental_species[s] = None
422 |
423 | unordered_species=species.copy()
424 | species={k:v for k, v in fundamental_species.items()}
425 | species.update({k:v for k,v in unordered_species.items() if k not in fundamental_species.keys()})
426 | return species, fundamental_species
427 |
428 |
429 | if __name__ == "__main__":
430 | custom_system = """
431 | P+P<->PP
432 | P+L<->PL
433 | PP+L<->PPL1
434 | PP+L<->PPL2
435 | PPL1+L<->PPL1L2
436 | PPL2+L<->PPL1L2
437 | """
438 | custom_generated_binding_system = MinimizerBindingSystemFactory(
439 | custom_system, output_filename="tmp.py"
440 | )
441 | print(custom_generated_binding_system.binding_function_arguments)
442 | print(custom_generated_binding_system.binding_function(10,10,10,10,10,10,10,10))
443 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/minimizer_systems.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from .binding_system import BindingSystem
3 | from mpmath import mpf, findroot, mp, almosteq
4 | from .minimizer_binding_system_factory import MinimizerBindingSystemFactory
5 |
6 | mpf_zero=mpf(0)
7 | mpf_tol=mpf("1e-10")
8 | max_iters=1e6
9 |
10 | # 1:1 binding - see https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
11 | def system01_minimizer(p, l, kdpl):
12 | p = mpf(p)
13 | l = mpf(l)
14 | kdpl = mpf(kdpl)
15 | if almosteq(kdpl, mpf_zero, mpf_tol):
16 | kdpl+=mpf_tol
17 | def f(p_f, l_f):
18 | pl = p_f * l_f / kdpl
19 | return p - (p_f + pl), l - (l_f + pl)
20 | p_f, l_f = findroot(f, [mpf_zero, mpf_zero], tol=mpf_tol, maxsteps=1e6)
21 | return {"pf": p_f, "lf": l_f, "pl": (p_f * l_f) / kdpl}
22 |
23 |
24 | # 1:1:1 competition - see https://stevenshave.github.io/pybindingcurve/simulate_competition.html
25 | def system02_minimizer(p, l, i, kdpl, kdpi):
26 | kdpl = mpf(kdpl)
27 | kdpi = mpf(kdpi)
28 | if almosteq(kdpl, mpf_zero, mpf_tol):
29 | kdpl+=mpf_tol
30 | if almosteq(kdpi, mpf_zero, mpf_tol):
31 | kdpi+=mpf_tol
32 | p = mpf(p)
33 | l = mpf(l)
34 | i = mpf(i)
35 | def f(p_f, l_f, i_f):
36 | pl = p_f * l_f / kdpl
37 | pi = p_f * i_f / kdpi
38 | return p - (p_f + pl + pi), l - (l_f + pl), i - (i_f + pi)
39 | p_f, l_f, i_f = findroot(f, [mpf_zero, mpf_zero, mpf_zero], tol=mpf_tol, maxsteps=1e6)
40 | return {
41 | "pf": p_f,
42 | "lf": l_f,
43 | "if": i_f,
44 | "pl": (p_f * l_f) / kdpl,
45 | "pi": (p_f * i_f) / kdpi,
46 | }
47 |
48 |
49 | # Homodimer formation - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
50 | def system03_minimizer(p, kdpp):
51 | p = mpf(p)
52 | kdpp = mpf(kdpp)
53 | if almosteq(kdpp, mpf_zero, mpf_tol):
54 | kdpp+=mpf_tol
55 | def f(p_f):
56 | pp = p_f * p_f / kdpp
57 | return p - (p_f + 2 * pp)
58 | p_f = findroot(f, [mpf_zero], tol=mpf_tol, maxsteps=1e6)
59 | return {"pf": p_f, "pp": (p_f * p_f) / kdpp}
60 |
61 |
62 | # Homodimer breaking - see https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
63 | def system04_minimizer(p, i, kdpp, kdpi):
64 | p = mpf(p)
65 | i = mpf(i)
66 | kdpp = mpf(kdpp)
67 | kdpi = mpf(kdpi)
68 | if almosteq(kdpp, mpf_zero, mpf_tol):
69 | kdpp+=mpf_tol
70 | if almosteq(kdpi, mpf_zero, mpf_tol):
71 | kdpi+=mpf_tol
72 | def f(p_f, i_f):
73 | pp = p_f * p_f / kdpp
74 | pi = p_f * i_f / kdpi
75 | return p - (p_f + pi + 2 * pp), i - (i_f + pi)
76 | p_f, i_f = findroot(f, (mpf_zero,mpf_zero), tol=mpf_tol, maxsteps=1e6)
77 | return {"pf": p_f, "if": i_f, "pp": (p_f * p_f) / kdpp, "pi": (p_f * i_f) / kdpi}
78 |
79 |
80 | # 1:2 binding
81 | def system12_minimizer(p, l, kdpl1, kdpl2):
82 | p = mpf(p)
83 | l = mpf(l)
84 | kdpl1 = mpf(kdpl1)
85 | kdpl2 = mpf(kdpl2)
86 | if almosteq(kdpl1, mpf_zero, mpf_tol):
87 | kdpl1+=mpf_tol
88 | if almosteq(kdpl2, mpf_zero, mpf_tol):
89 | kdpl2+=mpf_tol
90 | def f(p_f, l_f):
91 | pl1 = p_f * l_f / kdpl1
92 | pl2 = p_f * l_f / kdpl2
93 | pl12 = (pl1 * l_f + pl2 * l_f) / (kdpl1 + kdpl2)
94 | return p - (p_f + pl1 + pl2 + pl12), l - (l_f + pl1 + pl2 + 2 * pl12)
95 | p_f, l_f = findroot(f, [mpf_zero, mpf_zero], tol=mpf_tol, maxsteps=1e6)
96 | pl1 = (p_f * l_f) / kdpl1
97 | pl2 = (p_f * l_f) / kdpl2
98 | return {
99 | "pf": p_f,
100 | "lf": l_f,
101 | "pl1": pl1,
102 | "pl2": pl2,
103 | "pl12": (pl1 * l_f + pl2 * l_f) / (kdpl1 + kdpl2),
104 | }
105 |
106 |
107 | # 1:3 binding
108 | def system13_minimizer(p, l, kdpl1, kdpl2, kdpl3):
109 | p = mpf(p)
110 | l = mpf(l)
111 | kdpl1 = mpf(kdpl1)
112 | kdpl2 = mpf(kdpl2)
113 | kdpl3 = mpf(kdpl3)
114 | if almosteq(kdpl1, mpf_zero, mpf_tol):
115 | kdpl1+=mpf_tol
116 | if almosteq(kdpl2, mpf_zero, mpf_tol):
117 | kdpl2+=mpf_tol
118 | if almosteq(kdpl3, mpf_zero, mpf_tol):
119 | kdpl3+=mpf_tol
120 | def f(p_f, l_f):
121 | pl1 = p_f * l_f / kdpl1
122 | pl2 = p_f * l_f / kdpl2
123 | pl3 = p_f * l_f / kdpl3
124 | pl12 = (pl1 * l_f + pl2 * l_f) / (kdpl1 + kdpl2)
125 | pl23 = (pl2 * l_f + pl3 * l_f) / (kdpl2 + kdpl3)
126 | pl13 = (pl1 * l_f + pl3 * l_f) / (kdpl1 + kdpl3)
127 | pl123 = (pl12 * l_f + pl23 * l_f + pl13 * l_f) / (kdpl1 + kdpl2 + kdpl3)
128 | return p - (p_f + pl1 + pl2 + pl3 + pl12 + pl13 + pl23), l - (
129 | l_f + pl1 + pl2 + pl3 + 2 * (pl12 + pl13 + pl23) + 3 * pl123
130 | )
131 | p_f, l_f = findroot(f, [mpf_zero, mpf_zero], tol=mpf_tol, maxsteps=1e6)
132 | pl1 = p_f * l_f / kdpl1
133 | pl2 = p_f * l_f / kdpl2
134 | pl3 = p_f * l_f / kdpl3
135 | pl12 = (pl1 * l_f + pl2 * l_f) / (kdpl1 + kdpl2)
136 | pl23 = (pl2 * l_f + pl3 * l_f) / (kdpl2 + kdpl3)
137 | pl13 = (pl1 * l_f + pl3 * l_f) / (kdpl1 + kdpl3)
138 | return {
139 | "pf": p_f,
140 | "lf": l_f,
141 | "pl1": pl1,
142 | "pl2": pl2,
143 | "pl3": pl3,
144 | "pl12": pl12,
145 | "pl13": pl13,
146 | "pl23": pl23,
147 | "pl123": (pl12 * l_f + pl23 * l_f + pl13 * l_f) / (kdpl1 + kdpl2 + kdpl3),
148 | }
149 |
150 | class System_minimizer_one_to_one__pl(BindingSystem):
151 | """
152 | Minimizer-based one to one binding
153 |
154 | Class defines 1:1 binding, readout is PL
155 | See https://stevenshave.github.io/pybindingcurve/simulate_1to1.html
156 | """
157 |
158 | def __init__(self):
159 | super().__init__(system01_minimizer, False)
160 | self.default_readout = "pl"
161 |
162 | def query(self, parameters: dict):
163 | mp.dps = 100
164 | if self._are_ymin_ymax_present(parameters):
165 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
166 | parameters
167 | )
168 | value = super().query(parameters_no_min_max)
169 | with np.errstate(divide="ignore", invalid="ignore"):
170 | return np.nan_to_num(
171 | parameters["ymin"]
172 | + ((parameters["ymax"] - parameters["ymin"]) * value)
173 | / (parameters["p"] / 2.0)
174 | )
175 | else:
176 | return super().query(parameters)
177 |
178 |
179 | class System_minimizer_homodimerformation__pp(BindingSystem):
180 | """
181 | Minimizer-based homodimer formation system
182 |
183 | Class defines homodimer formation, readout is PP
184 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerformation.html
185 | """
186 |
187 | def __init__(self):
188 | super().__init__(system03_minimizer, False)
189 | self.default_readout = "pp"
190 |
191 | def query(self, parameters: dict):
192 | mp.dps = 100
193 | if self._are_ymin_ymax_present(parameters):
194 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
195 | parameters
196 | )
197 | value = super().query(parameters_no_min_max)
198 | with np.errstate(divide="ignore", invalid="ignore"):
199 | return np.nan_to_num(
200 | parameters["ymin"]
201 | + ((parameters["ymax"] - parameters["ymin"]) * value)
202 | / (parameters["p"] / 2.0)
203 | )
204 | else:
205 | return super().query(parameters)
206 |
207 |
208 | class System_minimizer_competition__pl(BindingSystem):
209 | """
210 | Minimizer-based competition system
211 |
212 | Class defines 1:1:1 competition, readout is PL
213 | See https://stevenshave.github.io/pybindingcurve/simulate_competition.html
214 | """
215 |
216 | def __init__(self):
217 | super().__init__(system02_minimizer, False)
218 | self.default_readout = "pl"
219 |
220 | def query(self, parameters: dict):
221 | mp.dps = 100
222 | if self._are_ymin_ymax_present(parameters):
223 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
224 | parameters
225 | )
226 | value = super().query(parameters_no_min_max)
227 | with np.errstate(divide="ignore", invalid="ignore"):
228 | return np.nan_to_num(
229 | parameters["ymin"]
230 | + ((parameters["ymax"] - parameters["ymin"]) * value)
231 | / (parameters["p"] / 2.0)
232 | )
233 | else:
234 | return super().query(parameters)
235 |
236 |
237 | class System_minimizer_homodimerbreaking__pp(BindingSystem):
238 | """
239 | Minimizer-based homodimer breaking system
240 |
241 | Class defines homodimer breaking, readout is PP
242 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
243 | """
244 |
245 | def __init__(self):
246 | super().__init__(system04_minimizer, False)
247 | self.default_readout = "pp"
248 |
249 | def query(self, parameters: dict):
250 | mp.dps = 100
251 | if self._are_ymin_ymax_present(parameters):
252 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
253 | parameters
254 | )
255 | value = super().query(parameters_no_min_max)
256 | with np.errstate(divide="ignore", invalid="ignore"):
257 | return np.nan_to_num(
258 | parameters["ymin"]
259 | + ((parameters["ymax"] - parameters["ymin"]) * value)
260 | / (parameters["p"] / 2.0)
261 | )
262 | else:
263 | return super().query(parameters)
264 |
265 |
266 | class System_minimizer_homodimerbreaking__pl(BindingSystem):
267 | """
268 | Minimzer-based homodimer breaking system
269 |
270 | Class defines homodimer breaking, readout is PL
271 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
272 | """
273 |
274 | def __init__(self):
275 | super().__init__(system04_minimizer, False)
276 | self.default_readout = "pl"
277 |
278 | def query(self, parameters: dict):
279 | mp.dps = 100
280 | if self._are_ymin_ymax_present(parameters):
281 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
282 | parameters
283 | )
284 | value = super().query(parameters_no_min_max)
285 | with np.errstate(divide="ignore", invalid="ignore"):
286 | return np.nan_to_num(
287 | parameters["ymin"]
288 | + ((parameters["ymax"] - parameters["ymin"]) * value)
289 | / (parameters["p"] / 2.0)
290 | )
291 | else:
292 | return super().query(parameters)
293 |
294 |
295 | class System_minimizer_1_to_2__pl12(BindingSystem):
296 | """
297 | Lagrange 1:2 binding system.
298 |
299 | Class defines 1:2 protein:ligand binding, readout is PL12, meaning protein
300 | with 2 ligands
301 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
302 | """
303 |
304 | def __init__(self):
305 | super().__init__(system12_minimizer, False)
306 | self.default_readout = "pl12"
307 |
308 | def query(self, parameters: dict):
309 | mp.dps = 100
310 | if self._are_ymin_ymax_present(parameters):
311 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
312 | parameters
313 | )
314 | value = super().query(parameters_no_min_max)
315 | with np.errstate(divide="ignore", invalid="ignore"):
316 | return np.nan_to_num(
317 | parameters["ymin"]
318 | + ((parameters["ymax"] - parameters["ymin"]) * value)
319 | / parameters["l"]
320 | )
321 | else:
322 | return super().query(parameters)
323 |
324 |
325 | class System_minimizer_1_to_3__pl123(BindingSystem):
326 | """
327 | Lagrange 1:2 binding system.
328 |
329 | Class defines 1:2 protein:ligand binding, readout is PL123, meaning protein
330 | with 3 ligands
331 | See https://stevenshave.github.io/pybindingcurve/simulate_homodimerbreaking.html
332 | """
333 |
334 | def __init__(self):
335 | super().__init__(system13_minimizer, False)
336 | self.default_readout = "pl123"
337 |
338 | def query(self, parameters: dict):
339 | mp.dps = 100
340 | if self._are_ymin_ymax_present(parameters):
341 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
342 | parameters
343 | )
344 | value = super().query(parameters_no_min_max)
345 | with np.errstate(divide="ignore", invalid="ignore"):
346 | return np.nan_to_num(
347 | parameters["ymin"]
348 | + ((parameters["ymax"] - parameters["ymin"]) * value)
349 | / parameters["l"]
350 | )
351 | else:
352 | return super().query(parameters)
353 |
354 |
355 | class System_minimizer_custom(BindingSystem):
356 | """
357 | Lagrange custom binding system
358 |
359 | Class uses LagrangeBindingSystemFactory to make a custom lagrange function.
360 | """
361 |
362 | def __init__(self, system_string):
363 | custom_system = MinimizerBindingSystemFactory(system_string)
364 | super().__init__(custom_system.binding_function)
365 | self.all_species=custom_system.all_species
366 | self.default_readout = custom_system.readout
367 | self.arguments=custom_system.binding_function_arguments
368 |
369 | def query(self, parameters: dict):
370 | mp.dps = 100
371 | if self._are_ymin_ymax_present(parameters):
372 | parameters_no_min_max = self._remove_ymin_ymax_keys_from_dict_return_new(
373 | parameters
374 | )
375 | value = super().query(parameters_no_min_max)
376 | with np.errstate(divide="ignore", invalid="ignore"):
377 | return np.nan_to_num(
378 | parameters["ymin"]
379 | + ((parameters["ymax"] - parameters["ymin"]) * value)
380 | / parameters["l"]
381 | )
382 | else:
383 | return super().query(parameters)
384 |
--------------------------------------------------------------------------------
/src/pybindingcurve/systems/systems.py:
--------------------------------------------------------------------------------
1 | from pybindingcurve.systems import analyticalsystems, kineticsystems, minimizer_systems
2 | from inspect import signature
3 | import numpy as np
4 | import mpmath
5 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import numpy as np
3 |
4 | @pytest.fixture
5 | def data_res_one_to_one():
6 | """Classification dataset, 7 classes, 7 clusters, 100 samples, 10 features"""
7 | return np.array(
8 | [
9 | 0.0, 0.36976681,0.73678745,1.10079404,1.46148871,1.81854052,2.17158225,
10 | 2.52020739,2.86396726,3.20236871,3.5348727,3.8608942,4.17980399,
11 | 4.49093304,4.79358009,5.08702315,5.37053529,5.64340495,5.90496012,
12 | 6.15459543,6.39180018,6.61618461,6.82750179,7.0256622,7.21073915,
13 | 7.38296437,7.5427142,7.69048839,7.82688409,7.95256788,8.06824855,
14 | 8.17465245,8.27250287,8.36250371,8.44532763,8.52160808,8.59193462,
15 | 8.65685092,8.71685466,8.77239888,8.8238942,8.87171161,8.91618555,
16 | 8.95761706,8.99627687,9.03240837,9.06623039,9.09793973,9.12771349,
17 | 9.15571123,
18 | ]
19 | )
20 |
21 | @pytest.fixture
22 | def data_res_competition():
23 | """Classification dataset, 7 classes, 7 clusters, 100 samples, 10 features"""
24 | return np.array([
25 | 4.33809621, 4.22751888, 4.11497749, 4.00061453, 3.88460108, 3.76713883,
26 | 3.64846169, 3.52883679, 3.4085647, 3.28797858, 3.16744212, 3.04734594,
27 | 2.92810239, 2.81013866, 2.69388832, 2.57978151, 2.46823425, 2.35963741,
28 | 2.25434621, 2.15267084, 2.05486895, 1.96114064, 1.87162611, 1.78640606,
29 | 1.70550464, 1.62889436, 1.55650261, 1.48821894, 1.42390269, 1.36339043,
30 | 1.30650277, 1.25305051, 1.20283978, 1.1556763, 1.11136872, 1.06973114,
31 | 1.03058497, 0.99376017, 0.95909597, 0.9264413, 0.8956548, 0.86660478,
32 | 0.83916885, 0.81323356, 0.78869392, 0.76545293, 0.74342101, 0.72251552,
33 | 0.70266024, 0.68378487
34 | ])
--------------------------------------------------------------------------------
/tests/test_competition.py:
--------------------------------------------------------------------------------
1 | """
2 | pytest tests for PyBindingCurve
3 |
4 |
5 | PyBindingCurve source may be tested to ensure internal consistency (agreement)
6 | amongst simulation methods, and externally consistent (agreement with)
7 | literature values. With pytest installed in the local python environment
8 | (pip install pytest), simply run 'pytest' to run the testsuite.
9 | """
10 |
11 | import pybindingcurve as pbc
12 | import numpy as np
13 |
14 | ##########################
15 | ### Test 1:1:1 competition
16 | ##########################
17 |
18 | # Default
19 | def test_competition_simulation_default(data_res_competition):
20 | my_system = pbc.BindingCurve("competition")
21 | assert np.sum(np.abs(my_system.query({"p": 12, "l": 10, "i": np.linspace(0,25), "kdpi": 1, "kdpl": 10}) - data_res_competition)) < 1e-6
22 |
23 | # Default
24 | def test_competition_simulation_analytical(data_res_competition):
25 | my_system = pbc.BindingCurve("competitionanalytical")
26 | assert np.sum(np.abs(my_system.query({"p": 12, "l": 10, "i": np.linspace(0,25), "kdpi": 1, "kdpl": 10}) - data_res_competition)) < 1e-6
27 |
28 | # Minimizer
29 | def test_competition_simulation_minimzer(data_res_competition):
30 | my_system = pbc.BindingCurve("competitionmin")
31 | assert np.sum(np.abs(my_system.query({"p": 12, "l": 10, "i": np.linspace(0,25), "kdpi": 1, "kdpl": 10}) - data_res_competition)) < 1e-6
32 |
33 | # Kinetic
34 | def test_competition_simulation_kinetic(data_res_competition):
35 | my_system = pbc.BindingCurve("competitionkinetic")
36 | assert np.sum(np.abs(my_system.query({"p": 12, "l": 10, "i": np.linspace(0,25), "kdpi": 1, "kdpl": 10}) - data_res_competition)) < 1e-6
37 |
38 | # Custom system definition
39 | def test_competition_simulation_custom_definition(data_res_competition):
40 | my_system = pbc.BindingCurve("p+l<->pl*,p+i<->pi")
41 | assert np.sum(np.abs(my_system.query({"p": 12, "l": 10, "i": np.linspace(0,25), "kd_p_i_pi": 1, "kd_p_l_pl": 10}) - data_res_competition)) < 1e-6
42 |
--------------------------------------------------------------------------------
/tests/test_fitting.py:
--------------------------------------------------------------------------------
1 | """
2 | pytest tests for PyBindingCurve
3 |
4 | PyBindingCurve source may be tested to ensure internal consistency (agreement)
5 | amongst simulation methods, and externally consistent (agreement with)
6 | literature values. With pytest installed in the local python environment
7 | (pip install pytest), simply run 'pytest' to run the testsuite.
8 | """
9 | import pytest
10 | import pybindingcurve as pbc
11 | import numpy as np
12 |
13 | # Testing of fits.
14 | # lmfit returns proper 95 % confidence intervals for where the real KD value is.
15 | # Taking into account this +/- amount is difficult.
16 |
17 | def test_1_to_1_fit_default():
18 | xcoords = np.array([0.0, 20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0])
19 | ycoords = np.array([0.544, 4.832, 6.367, 7.093, 7.987, 9.005, 9.079, 8.906, 9.010, 10.046, 9.225])
20 | my_system = pbc.BindingCurve("1:1")
21 | system_parameters = {"p": xcoords, "l": 10}
22 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpl": 0}, ycoords)
23 | assert pytest.approx(fitted_system["kdpl"])==16.3715989783099
24 |
25 | def test_1_to_1_fit_analytical():
26 | xcoords = np.array([0.0, 20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0])
27 | ycoords = np.array([0.544, 4.832, 6.367, 7.093, 7.987, 9.005, 9.079, 8.906, 9.010, 10.046, 9.225])
28 | my_system = pbc.BindingCurve("1:1analytical")
29 | system_parameters = {"p": xcoords, "l": 10}
30 | fitted_system, fit_accuracy = my_system.fit(system_parameters, {"kdpl": 0}, ycoords)
31 | assert pytest.approx(fitted_system["kdpl"])==16.3715989783099
32 |
--------------------------------------------------------------------------------
/tests/test_one_to_one.py:
--------------------------------------------------------------------------------
1 | """
2 | pytest tests for PyBindingCurve
3 |
4 |
5 | PyBindingCurve source may be tested to ensure internal consistency (agreement)
6 | amongst simulation methods, and externally consistent (agreement with)
7 | literature values. With pytest installed in the local python environment
8 | (pip install pytest), simply run 'pytest' to run the testsuite.
9 | """
10 |
11 | import pybindingcurve as pbc
12 | import numpy as np
13 |
14 | ########################
15 | ### Test 1:1 simulation
16 | ########################
17 |
18 | # Default
19 | def test_1_to_1_simulation_default_approach(data_res_one_to_one):
20 | my_system = pbc.BindingCurve("1:1")
21 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kdpl": 1}) - data_res_one_to_one)) < 1e-6
22 |
23 | # Analytical
24 | def test_1_to_1_simulation_analytical(data_res_one_to_one):
25 | my_system = pbc.BindingCurve("1:1analytical")
26 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kdpl": 1}) - data_res_one_to_one)) < 1e-6
27 |
28 | # Minimizer
29 | def test_1_to_1_simulation_minimizer(data_res_one_to_one):
30 | my_system = pbc.BindingCurve("1:1min")
31 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kdpl": 1}) - data_res_one_to_one)) < 1e-6
32 |
33 | # Lagrange
34 | def test_1_to_1_simulation_lagrange(data_res_one_to_one):
35 | my_system = pbc.BindingCurve("1:1lagrange")
36 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kdpl": 1}) - data_res_one_to_one)) < 1e-6
37 |
38 | # Kinetic
39 | def test_1_to_1_simulation_kinetic(data_res_one_to_one):
40 | my_system = pbc.BindingCurve("1:1kinetic")
41 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kdpl": 1}) - data_res_one_to_one)) < 1e-6
42 |
43 | # Custom system definition
44 | def test_1_to_1_simulation_custom_definition(data_res_one_to_one):
45 | my_system = pbc.BindingCurve("P+L<->PL*")
46 | assert np.sum(np.abs(my_system.query({"p": np.linspace(0, 20), "l": 10, "kd_p_l_pl": 1}) - data_res_one_to_one)) < 1e-6
47 |
--------------------------------------------------------------------------------
/utils/example-DisplayHomoVsHetroDimerComparisonHeatmaps.py:
--------------------------------------------------------------------------------
1 | # %%
2 | import numpy as np
3 | import matplotlib.pyplot as plt
4 | import seaborn as sns
5 | import pickle
6 | import sys
7 | from pathlib import Path
8 |
9 |
10 | def load_heatmap(filename):
11 | file = Path(filename)
12 | if file.exists():
13 | mat = pickle.load(open(filename, "rb"))
14 | return mat
15 | return None
16 |
17 |
18 | heatmaps_homo = []
19 | heatmaps_hetero = []
20 |
21 | [heatmaps_homo.append(load_heatmap(f)) for f in ["heatmaphomo-5.0.pkl"]]
22 | [heatmaps_hetero.append(load_heatmap(f)) for f in ["heatmaphetero-5.0.pkl"]]
23 |
24 | print(heatmaps_hetero)
25 |
26 | fig, ax = plt.subplots(
27 | nrows=len(heatmaps_hetero), ncols=3, figsize=(10, 4), sharey=True, sharex=True
28 | )
29 |
30 | # colour_map=sns.color_palette("Greys", 70)
31 | colour_map = sns.color_palette("RdBu_r", 256)
32 |
33 | for i in range(len(heatmaps_hetero)):
34 | # ax[i,0].imshow(heatmaps_homo[i])
35 | sns.heatmap(
36 | heatmaps_homo[i][:, ::-1],
37 | ax=ax[0],
38 | vmin=0,
39 | vmax=1,
40 | cmap=colour_map,
41 | cbar=True,
42 | cbar_kws=dict(pad=0.01),
43 | )
44 | sns.heatmap(
45 | heatmaps_hetero[i][:, ::-1],
46 | ax=ax[1],
47 | vmin=0,
48 | vmax=1,
49 | cmap=colour_map,
50 | cbar=True,
51 | cbar_kws=dict(pad=0.01),
52 | )
53 | sns.heatmap(
54 | heatmaps_homo[i][:, ::-1] - heatmaps_hetero[i][:, ::-1],
55 | ax=ax[2],
56 | cmap=sns.color_palette("PRGn", 1024),
57 | center=0,
58 | cbar=True,
59 | cbar_kws=dict(pad=0.01),
60 | )
61 | ax[2].get_yaxis().set_visible(False)
62 |
63 |
64 | x_labels = ["9 (nM)", "6 ($\mathrm{\mu}$M)", "3 (mM)"]
65 | x_labels.reverse()
66 | y_labels = x_labels.copy()
67 |
68 | y_labels.reverse()
69 | y_labels[2] = " 3 (mM)"
70 | for axx in ax:
71 | plt.xticks(
72 | np.arange(0, heatmaps_hetero[0].shape[1] + 1, heatmaps_hetero[0].shape[1] / 2),
73 | rotation=0,
74 | )
75 | plt.yticks(
76 | np.arange(0, heatmaps_hetero[0].shape[1] + 1, heatmaps_hetero[0].shape[0] / 2)
77 | )
78 | axx.set_xticks = np.arange(len(x_labels))
79 | axx.set_yticks = np.arange(len(y_labels))
80 | axx.set_xticklabels(x_labels, rotation=0)
81 | axx.set_yticklabels(y_labels, rotation=0)
82 | axx.patch.set_linewidth("1")
83 | axx.patch.set_edgecolor("black")
84 |
85 | fig.text(0.45, 0.02, r"p$\mathrm{K_DI}$")
86 | fig.text(0.008, 0.545, r"p$\mathrm{K_DDimer}$", rotation=90)
87 | fig.text(0.165, 0.805, "Homodimer")
88 | fig.text(0.465, 0.805, "Heterodimer")
89 | fig.text(0.68, 0.805, "Difference (Homodimer-Heterodimer)")
90 |
91 | plt.suptitle(
92 | r"Dimer breaking with inhibitor, 2 $\mathrm{\mu}$M homodimer monomer and"
93 | + "\n"
94 | + r"1+1 $\mathrm{\mu}$M heterodimer monomers, 5 $\mathrm{\mu}$M inhibitor",
95 | fontsize=16,
96 | )
97 | plt.tight_layout(rect=(0.01, 0.025, 1, 0.85), w_pad=-0.3)
98 | plt.show()
99 |
--------------------------------------------------------------------------------
/utils/example-GenerateHomoVsHetroDimerComparisonHeatmaps.py:
--------------------------------------------------------------------------------
1 | #%%
2 | from multiprocessing import Pool
3 | import numpy as np
4 | import pybindingcurve as pbc
5 | import pickle
6 | from pathlib import Path
7 |
8 | plot_steps = 10
9 |
10 |
11 | def get_2D_grid_values(
12 | xmin,
13 | xmax,
14 | ymin,
15 | ymax,
16 | system,
17 | parameters,
18 | x_parameter,
19 | y_parameter,
20 | filename,
21 | plot_steps,
22 | starting_y_guess=0,
23 | ):
24 | file = Path(filename)
25 | if file.exists():
26 | mat = pickle.load(open(filename, "rb"))
27 | return mat
28 | x_logconc = np.linspace(xmin, xmax, plot_steps)
29 | y_logconc = np.linspace(ymin, ymax, plot_steps)
30 | mat = np.ndarray((plot_steps, plot_steps))
31 | for ix, x in enumerate(x_logconc):
32 | print("Working on :", x)
33 | for iy, y in enumerate(y_logconc):
34 | parameters[x_parameter] = 10 ** x
35 | parameters[y_parameter] = 10 ** y
36 | if "homo" in filename:
37 | mat[ix, iy] = system.query(parameters, "pp")
38 | else:
39 | mat[ix, iy] = system.query(parameters, "pl")
40 |
41 | pickle.dump(mat, open(filename, "wb"))
42 | return mat
43 |
44 |
45 | pool = Pool(2)
46 |
47 | for i_amount in [5.0]:
48 | j1 = pool.apply_async(
49 | get_2D_grid_values,
50 | [
51 | -3,
52 | 3,
53 | -3,
54 | 3,
55 | pbc.System_homodimer_breaking(),
56 | {"p": 2, "i": i_amount},
57 | "kdpp",
58 | "kdpi",
59 | f"heatmaphomo-{str(i_amount)}.pkl",
60 | plot_steps,
61 | ],
62 | )
63 | j2 = pool.apply_async(
64 | get_2D_grid_values,
65 | [
66 | -3,
67 | 3,
68 | -3,
69 | 3,
70 | pbc.System_competition(),
71 | {"p": 1, "l": 1, "i": i_amount},
72 | "kdpl",
73 | "kdpi",
74 | f"heatmaphetero-{str(i_amount)}.pkl",
75 | plot_steps,
76 | ],
77 | )
78 | res = j1.get()
79 | res = j2.get()
80 |
--------------------------------------------------------------------------------
/utils/example-HomoVsHetroDimerFormationBreaking.py:
--------------------------------------------------------------------------------
1 | import matplotlib as mpl
2 | import numpy as np
3 | import pybindingcurve as pbc
4 | import matplotlib.pyplot as plt
5 | import seaborn as sns
6 | import pickle
7 | import matplotlib.ticker
8 | import sys
9 |
10 | num_points = 500
11 | max_x_breaking = 5
12 | max_x_formation = 1
13 | dimer_kd = 0.1
14 | inhibitor_kd = 10e-3
15 |
16 |
17 | # Calculate formation concentrations
18 | x_axis_formation = np.linspace(0, max_x_formation, num=num_points)
19 | x_axis_breaking = np.linspace(0, max_x_breaking, num=num_points)
20 | homo_y_formation = np.empty((num_points))
21 | hetero_y_formation = np.empty((num_points))
22 | for i in range(len(homo_y_formation)):
23 | homo_y_formation[i] = pbc.systems.System_analytical_homodimerformation_pp().query(
24 | {"kdpp": dimer_kd, "p": x_axis_formation[i] * 2}
25 | )
26 | hetero_y_formation[i] = pbc.systems.System_analytical_one_to_one_pl().query(
27 | {"kdpl": dimer_kd, "p": x_axis_formation[i], "l": x_axis_formation[i]}
28 | )
29 |
30 | homo_y_breaking = np.empty((num_points))
31 | hetero_y_breaking = np.empty((num_points))
32 | for i in range(len(homo_y_formation)):
33 | homo_y_breaking[i] = pbc.systems.System_kinetic_homodimerbreaking_pp().query(
34 | {"kdpi": inhibitor_kd, "kdpp": dimer_kd, "p": 2, "i": x_axis_breaking[i]}
35 | )
36 | hetero_y_breaking[i] = pbc.systems.System_analytical_competition_pl().query(
37 | {
38 | "kdpi": inhibitor_kd,
39 | "kdpl": dimer_kd,
40 | "p": 1,
41 | "l": 1,
42 | "i": x_axis_breaking[i],
43 | }
44 | )
45 |
46 | fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 5.5), sharey=True)
47 | # plt.tight_layout()
48 | fig.suptitle("Homo- vs Hetero-dimer", fontsize=18)
49 |
50 | ax[0].plot(
51 | x_axis_formation, homo_y_formation, "k", label="Homodimer monomers", linestyle="--"
52 | )
53 | ax[0].plot(x_axis_formation, hetero_y_formation, "k", label="Heterodimer monomers")
54 | ax[0].set_xlim(0, max_x_formation)
55 | ax[0].set_ylim(0, 1)
56 | ax[0].legend()
57 | ax[0].set_xlabel(r"[Monomers] ($\mathrm{\mu}$M)", fontsize=14)
58 | ax[0].set_ylabel(r"[Dimer] ($\mathrm{\mu}$M)", fontsize=14)
59 | ax[0].set_title(
60 | r"""Dimer formation,
61 | Dimer K$\mathrm{_D}$s = 100 nM""",
62 | fontsize=14,
63 | )
64 | ax[0].grid()
65 |
66 | ax[1].plot(
67 | x_axis_breaking,
68 | homo_y_breaking,
69 | "k",
70 | label=r"2 $\mathrm{\mu}$M homodimer monomer",
71 | linestyle="--",
72 | )
73 | ax[1].plot(
74 | x_axis_breaking,
75 | hetero_y_breaking,
76 | "k",
77 | label=r"1 $\mathrm{\mu}$M heterodimer monomers",
78 | )
79 | ax[1].set_xlim(0, max_x_breaking)
80 | ax[1].set_ylim(0, 1)
81 | ax[1].legend()
82 | ax[1].set_xlabel(r"[I$_0$] ($\mathrm{\mu}$M)", fontsize=14)
83 | # ax[1].set_ylabel(r"[Dimer] ($\mathrm{\mu}$M)", fontsize=14)
84 | ax[1].set_title(
85 | r"""Dimer breaking with inhibitor,
86 | Dimer K$\mathrm{_D}$s = 100 nM, inhibitor K$\mathrm{_D}$=10 nM""",
87 | fontsize=14,
88 | )
89 | ax[1].grid()
90 | plt.tight_layout(rect=(0, 0, 1, 0.9425), w_pad=-0.4)
91 |
92 | for axis in ax:
93 | axis.tick_params(axis="both", which="major", labelsize=14)
94 | axis.tick_params(axis="both", which="minor", labelsize=14)
95 | fig.text(0.05, 0.87, "A)", fontsize=20)
96 | fig.text(0.514, 0.87, "B)", fontsize=20)
97 | fig.savefig("/home/stevens/Downloads/fig1.svg", format="svg")
98 | plt.show()
99 |
--------------------------------------------------------------------------------