#!/usr/bin/env python # coding: utf-8 # ## Macromolecules : Binding Affinity and Fractional Occupancy # # # LAST REVISED: June 23, 2024 (using v. 1.0 beta36) # In[1]: import set_path # Importing this module will add the project's home directory to sys.path # In[2]: from life123 import ChemData from life123 import UniformCompartment from life123 import MovieTabular import numpy as np import plotly.express as px # In[3]: # Initialize the system chem = ChemData(names=["A", "B", "C"]) # ## Explore methods to manage the data structure for macromolecules # In[4]: chem.add_macromolecules("M1") chem.get_macromolecules() # In[5]: chem.set_binding_site_affinity("M1", site_number=3, ligand="A", Kd=1.0) chem.set_binding_site_affinity("M1", site_number=8, ligand="B", Kd=3.2) chem.set_binding_site_affinity("M1", site_number=15, ligand="A", Kd=10.0) chem.set_binding_site_affinity("M2", site_number=1, ligand="C", Kd=5.6) # "M2" will get automatically added chem.set_binding_site_affinity("M2", site_number=2, ligand="A", Kd=0.01) # In[6]: chem.show_binding_affinities() # Review the values we have given for the dissociation constants # In[7]: chem.get_binding_sites("M1") # In[8]: chem.get_binding_sites_and_ligands("M1") # In[9]: chem.get_binding_sites("M2") # In[10]: chem.get_binding_sites_and_ligands("M2") # In[11]: aff = chem.get_binding_site_affinity(macromolecule="M2", site_number=1) # A "NamedTuple" gets returned aff # In[12]: aff.chemical # In[13]: aff.Kd # In[ ]: # ## Start setting up the dynamical system # In[14]: dynamics = UniformCompartment(chem_data=chem) # In[15]: dynamics.set_macromolecules() # By default, set counts to 1 for all the registered macromolecules # In[16]: dynamics.describe_state() # ### Inspect some class attributes (not to be directly modified by the end user!) # In[17]: dynamics.macro_system # In[18]: dynamics.macro_system_state # ### Set the initial concentrations of all the ligands # In[19]: dynamics.set_conc(conc={"A": 10., "B": 0., "C": 0.56}) dynamics.describe_state() # ### Determine and adjust the fractional occupancy of the various sites on the macromolecules, based on the current ligand concentrations # In[20]: dynamics.update_occupancy() # In[21]: dynamics.describe_state() # In[22]: dynamics.chem_data.show_binding_affinities() # Review the values we had given for the dissociation constants # #### Notes: # **[B] = 0** => Occupancy of binding site 8 of M1 is also zero # # **[A] = 10.0** : # * 10x the dissociation constant of A to site 3 of M1 (resulting in occupancy 0.9) # * same as the dissociation constant of A to site 15 of M1 (occupancy 0.5) # * 1,000x the dissociation constant of A to site 2 of M2 (occupancy almost 1, i.e. nearly saturated) # # # **[C] = 0.56** => 1/10 of the dissociation constant of C to site 1 of M2 (occupancy 0.1) # In[ ]: # ### Adjust the concentration of one ligand, [A], and update all the fractional occupancies accordingly # In[23]: dynamics.set_single_conc(conc=1000., species_name="A", snapshot=False) # In[24]: dynamics.update_occupancy() # In[25]: dynamics.describe_state() # #### Note how all the various binding sites for ligand A, across all macromolecules, now have a different value for the fractional occupancy (very close to 1 because of the large value of [A] relative to each of the dissociation constants for A.) # The fractional occupancies for the other ligands (B and C) did not change # In[ ]: # ### Sweep the values of [A] across a wide range, and compute/store how the fractional occupancies of A change # In[26]: history = MovieTabular(parameter_name="[A]") # A convenient way to store a sequence of "state snapshots" as a Pandas dataframe # In[27]: print(history) # In[28]: # Generate a sweep of [A] values along a log scale, from very low to very high (relative to the dissociation constants) start = 0.001 stop = 200. num_points = 100 log_values = np.logspace(np.log10(start), np.log10(stop), num=num_points) print(log_values) # In[29]: # Set [A] to each of the above values in turn, and determine/store the applicable fractional occupancies (for the sites where A binds) for A_conc in log_values: dynamics.set_single_conc(conc=A_conc, species_name="A", snapshot=False) dynamics.update_occupancy() history.store(A_conc, {"M1 site 3": dynamics.get_occupancy(macromolecule="M1", site_number=3), "M1 site 15": dynamics.get_occupancy(macromolecule="M1", site_number=15), "M2 site 2": dynamics.get_occupancy(macromolecule="M2", site_number=2)}) # In[30]: df = history.get_dataframe() df # In[31]: # Plot each of the fractional occupancies as a function of [A] fig = px.line(data_frame=df, x="[A]", y=["M2 site 2", "M1 site 3", "M1 site 15"], color_discrete_sequence = ["seagreen", "purple", "darkorange"], title="Fractional Occupancy as a function of Ligand Concentration", labels={"value":"Fractional Occupancy", "variable":"Binding site"}) fig.add_hline(y=0.5, line_width=1, line_dash="dot", line_color="gray") # Horizontal line at 50% occupancy fig.show() # In[32]: import plotly.graph_objects as go # In[33]: # Same plot, but use a log scale for [A] fig = px.line(data_frame=df, x="[A]", y=["M2 site 2", "M1 site 3", "M1 site 15"], color_discrete_sequence = ["seagreen", "purple", "darkorange"], log_x=True, range_x=[start,200], title="Fractional Occupancy as a function of Ligand Concentration
(log plot on x-axis. Highlighting 0.1/0.5/0.9 occupancies)", labels={"value":"Fractional Occupancy", "variable":"Binding site"}) # Horizontal lines fig.add_hline(y=0.1, line_width=1, line_dash="dot", line_color="gray") fig.add_hline(y=0.5, line_width=1, line_dash="dot", line_color="gray") fig.add_hline(y=0.9, line_width=1, line_dash="dot", line_color="gray") # Annotations (x values adjusted for the log scale) fig.add_annotation(x=-1.715, y=0.65, text="Dissociation constant: 0.01
(HIGHER Binding Affinity)", font=dict(size=12, color="seagreen"), showarrow=True, ax=-100, ay=-20) fig.add_annotation(x=0.3, y=0.65, text="Dissociation constant: 1", font=dict(size=12, color="purple"), showarrow=True, ax=-100, ay=-20) fig.add_annotation(x=0.6, y=0.3, text="Dissociation constant: 10
(LOWER Binding Affinity)", font=dict(size=12, color="darkorange"), showarrow=True, ax=100, ay=10) fig.add_annotation(x=1.8, y=0.51, text="50% OCCUPANCY", font=dict(size=14, color="gray"), bgcolor="white", opacity=0.8, showarrow=False) # Customize y-axis tick values additional_y_values = [0.1, 0.5, 0.9] # Additional values to show on y-axis fig.update_layout(yaxis={"tickvals": list(fig.layout.yaxis.domain) + additional_y_values}) # Add scatter points (dots) to the plot, to highlight the 0.1/0.5/0.9 occupancies fig.add_scatter(x=[0.01, 1, 10, 0.001, 0.1, 1., 0.1, 10, 100 ], y=[0.5, 0.5, 0.5, 0.1, 0.1, 0.1, 0.9, 0.9, 0.9], mode="markers", marker={"color": "red"}, name="key points") fig.show() # #### When the binding affinity is lower (i.e. higher Dissociation Constant, Kd, orange curve), it takes higher ligand concentrations to attain the same fractional occupancies # Note that fractional occupancy 0.1 occurs at ligand concentrations of 1/10 the dissociation constant (Kd); # occupancy 0.5 occurs at ligand concentrations equals to the dissociation constant; # occupancy 0.9 occurs at ligand concentrations of 10x the dissociation constant. # ## The above simulation captures what's shown on Fig. 3A of # #### https://doi.org/10.1146/annurev-cellbio-100617-062719 # ("Low-Affinity Binding Sites and the Transcription Factor Specificity Paradox in Eukaryotes"), a paper that guided this simulation # #### In upcoming versions of Life123, the fractional occupancy values will regulate the rates of reactions catalyzed by the macromolecules... # In[ ]: