├── .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 | ![PyBindingCurve simulation](https://raw.githubusercontent.com/stevenshave/pybindingcurve/master/pybindingcurve_logo.png "Breaking a dimer") 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 | ![email](https://raw.githubusercontent.com/stevenshave/pybindingcurve/master/email-address-image.gif) 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 | ![1:1 binding system](./images/Fig_system_1to1.png "1:1 binding system") 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 | ![1:1 fit](./images/Fig_1to1_fit.svg "1:1 fit") 76 | 77 | 78 | [Return to tutorials](tutorial.md) 79 | -------------------------------------------------------------------------------- /docs/fit_competition.md: -------------------------------------------------------------------------------- 1 | # Fitting to 1:1:1 competition 2 | ![1:1 binding system](./images/Fig_system_competition.png "1:1 binding system") 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 | ![Fitting competition data](./images/Fig_competition_fit.svg "Fitting competition data") 71 | 72 | 73 | [Return to tutorials](tutorial.md) 74 | -------------------------------------------------------------------------------- /docs/fit_homodimerbreaking.md: -------------------------------------------------------------------------------- 1 | # Fitting of data to homodimer formation 2 | ![Homodimer breaking system](./images/Fig_system_homodimerbreaking.png "Homodimer breaking system") 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 | ![Fitting to homodimer breaking data](./images/Fig_homodimerbreaking_fit.svg) 65 | 66 | 67 | [Return to tutorials](tutorial.md) 68 | -------------------------------------------------------------------------------- /docs/fit_homodimerformation.md: -------------------------------------------------------------------------------- 1 | # Fitting of data to homodimer formation 2 | ![homodimer formation system](./images/Fig_system_homodimerformation.png "homodimer formation system") 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 | ![Fitting data to homodimer formation](./images/Fig_homodimerformation_fit.svg) 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 | ![PyBindingCurve simulation](https://raw.githubusercontent.com/stevenshave/pybindingcurve/master/pybindingcurve_logo.png "Breaking a dimer") 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 | ![1:1 binding system](./images/Fig_system_1to1.png "1:1 binding system") 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 | ![Image showing simulation of 1:1 binding](./images/Fig_1to1_simulation.svg "1:1 simulation") 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 | ![Image showing 1:1 simulation with signal as the readout](./images/Fig_1to1_simulation_ymax.svg "1:1 simulation with signal as the readout") 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 | ![Image showing 1:1 simulation with fraction ligand bound as the readout](./images/Fig_1to1_simulation_fraction_l.svg "1:1 simulation with fraction ligand bound as the readout") 64 | 65 | [Return to tutorials](tutorial.md) -------------------------------------------------------------------------------- /docs/simulate_competition.md: -------------------------------------------------------------------------------- 1 | # 1:1:1 competition simulation 2 | ![1:1 binding system](./images/Fig_system_competition.png "1:1 binding system") 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 | ![Competition simulation](./images/Fig_competition_simulation.svg) 44 | 45 | 46 | [Return to tutorials](tutorial.md) -------------------------------------------------------------------------------- /docs/simulate_custom_system.md: -------------------------------------------------------------------------------- 1 | # Simulation of custom binding systems 2 | ![1:1 binding system](./images/Fig_system_custom.png "custom binding system") 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 | ![Image showing simulation of 1:1 binding](./images/Fig_custom_simulation.svg "custom simulation") 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 | ![Homodimer breaking system](./images/Fig_system_homodimerbreaking.png "Homodimer breaking system") 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 | ![Homodimer breaking simulation](./images/Fig_homodimerbreaking_simulation.svg "Simulation of homodimer breaking") 33 | 34 | 35 | [Return to tutorials](tutorial.md) -------------------------------------------------------------------------------- /docs/simulate_homodimerformation.md: -------------------------------------------------------------------------------- 1 | # Homodimer formation simulation 2 | ![homodimer formation system](./images/Fig_system_homodimerformation.png "homodimer formation system") 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 | ![Homodimer fomation plot](./images/Fig_homodimerformation_simulation.svg "Homodimer formation plot") 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 | ![1:1 Binding System](./images/Fig_system_1to1.png) 7 | - [Simulation](simulate_1to1.md) 8 | - [Fitting](fit_1to1.md) 9 | 10 | 11 | ## 1:1:1 Competition 12 | ![1:1:1 Competition binding system](./images/Fig_system_competition.png) 13 | - [Simulation](simulate_competition.md) 14 | - [Fitting](fit_competition.md) 15 | 16 | 17 | ## Homodimer formation 18 | ![1:1 Binding System](./images/Fig_system_homodimerformation.png) 19 | - [Simulation](simulate_homodimerformation.md) 20 | - [Fitting](fit_homodimerformation.md) 21 | 22 | 23 | ## Homodimer breaking 24 | ![1:1 Binding System](./images/Fig_system_homodimerbreaking.png) 25 | - [Simulation](simulate_homodimerbreaking.md) 26 | - [Fitting](fit_homodimerbreaking.md) 27 | 28 | 29 | ## Custom system 30 | ![Custom Binding System](./images/Fig_system_custom.png) 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 | --------------------------------------------------------------------------------