diff --git a/Makefile b/Makefile index 52e5e79f..a26d5d9e 100644 --- a/Makefile +++ b/Makefile @@ -34,5 +34,5 @@ docs : cd docs; echo "Running Sphinx docs generator"; sphinx-apidoc -o source/ ../biocrnpyler; python generate_nblinks.py; make clean && make html; TAGS: biocrnpyler/*.py biocrnpyler/*/*.py biocrnpyler/*/*/*.py docs/*.rst \ - docs/examples/*.ipynb + docs/examples/*.ipynb docs/examples/*/*.ipynb ftags $^ diff --git a/Tests/test_combinatorial_complex.py b/Tests/test_combinatorial_complex.py index 8d582995..832c826c 100644 --- a/Tests/test_combinatorial_complex.py +++ b/Tests/test_combinatorial_complex.py @@ -401,3 +401,21 @@ def R(inputs, outputs): R([CXZ, X], [CXXZ]), ] assert all([r in r5_true for r in r5]) and all([r in r5 for r in r5_true]) + + +def test_singleton_arguments(): + X, Y, Z = Species('X'), Species('Y'), Species('Z') + + # Single final_state case + C1 = Complex([Y, Z, Z]) + CC1 = CombinatorialComplex(final_states=C1) + + # tests getters and setters + assert set(CC1.final_states) == set([C1]) + assert set(CC1.sub_species) == set([Y, Z]) + assert set(CC1.initial_states) == set([Y, Z]) + assert CC1.intermediate_states is None + + # Test with excluded states + C2 = Complex([X, X, Z, Z]) + CC5 = CombinatorialComplex(final_states=[C2], excluded_states=C1) diff --git a/biocrnpyler/components/basic.py b/biocrnpyler/components/basic.py index 7f77d4b4..59eaa7b4 100644 --- a/biocrnpyler/components/basic.py +++ b/biocrnpyler/components/basic.py @@ -9,21 +9,55 @@ class DNA(Component): - """DNA sequence that has a given length. + """DNA sequence component with specified length. + + A `DNA` component represents a DNA sequence with a given length in base + pairs. This component has no associated mechanism to generate species or + reactions, but can be used as a building block for more complex genetic + constructs. + + Parameters + ---------- + name : str + Name of the DNA sequence. + length : int, default=0 + Length of the DNA sequence in base pairs. + attributes : list of str, optional + List of attribute tags to associate with the DNA species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + species : Species + The DNA species object with material_type='dna'. + + See Also + -------- + RNA : RNA sequence component. + Protein : Protein sequence component. + Component : Base class for biomolecular components. + + Examples + -------- + Create a simple DNA sequence: + + >>> dna = bcp.DNA(name='my_gene', length=1000) + >>> dna.get_species() + dna_my_gene + + Create DNA with attributes: + + >>> promoter = bcp.DNA( + ... name='pLac', + ... length=100, + ... attributes=['inducible', 'strong'] + ... ) - Notes: - ----- - Produces no reactions. """ def __init__(self, name, length=0, attributes=None, **kwargs): - """Initialize a DNA object to store DNA related information. - - :param name: Name of the sequence (str) - :param length: length of the basepairs (int) - :param attributes: Species attribute - :param kwargs: pass into the parent's (Component) initializer - """ self.species = self.set_species( name, material_type='dna', attributes=attributes ) @@ -31,33 +65,91 @@ def __init__(self, name, length=0, attributes=None, **kwargs): super().__init__(name=name, **kwargs) def get_species(self) -> Species: + """Get the DNA species. + + Returns + ------- + Species + The DNA species object with material_type='dna'. + + """ return self.species def update_species(self) -> List[Species]: + """Generate species associated with the DNA component. + + Returns + ------- + list of Species + List containing only the DNA species itself, as DNA has no + associated mechanism to produce additional species. + + """ species = [self.get_species()] return species def update_reactions(self) -> List: + """Generate reactions associated with the DNA component. + + Returns + ------- + list + Empty list, as DNA has no associated mechanism. + + """ return [] class RNA(Component): - """RNA sequence of a given length. + """RNA sequence component with specified length. + + An `RNA` component represents an RNA sequence with a given length in base + pairs. This component has no associated mechanism to generate species or + reactions, but can be used to represent mRNA, tRNA, rRNA, or other RNA + molecules. + + Parameters + ---------- + name : str + Name of the RNA sequence. + length : int, default=0 + Length of the RNA sequence in base pairs. + attributes : list of str, optional + List of attribute tags to associate with the RNA species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + species : Species + The RNA species object with material_type='rna'. + + See Also + -------- + DNA : DNA sequence component. + Protein : Protein sequence component. + Component : Base class for biomolecular components. + + Examples + -------- + Create a simple RNA sequence: + + >>> rna = bcp.RNA(name='my_transcript', length=500) + >>> rna.get_species() + rna_my_transcript + + Create mRNA with attributes: + + >>> mrna = bcp.RNA( + ... name='gfp_mrna', + ... length=750, + ... attributes=['coding', 'stable'] + ... ) - Notes: - ----- - Produces no reactions. """ def __init__(self, name: str, length=0, attributes=None, **kwargs): - """Initialize an RNA object to store RNA related information. - - :param name: name of the rna - :param length: number of basepairs (int) - :param attributes: Species attribute - :param kwargs: pass into the parent's (Component) initializer - - """ self.species = self.set_species( name, material_type='rna', attributes=attributes ) @@ -65,34 +157,94 @@ def __init__(self, name: str, length=0, attributes=None, **kwargs): super().__init__(name=name, **kwargs) def get_species(self) -> Species: + """Get the RNA species. + + Returns + ------- + Species + The RNA species object with material_type='rna'. + + """ return self.species def update_species(self) -> List[Species]: + """Generate species associated with the RNA component. + + Returns + ------- + list of Species + List containing only the RNA species itself, as RNA has not + associated mechanism to produce additional species. + + """ species = [self.get_species()] return species def update_reactions(self) -> List: + """Generate reactions associated with the RNA component. + + Returns + ------- + list + Empty list, as RNA has no associated mechanism. + + """ return [] class Protein(Component): - """Protein/peptide of a given length. - - Notes: - ----- - Produces no reactions. + """Protein component with specified length. + + A `Protein` component represents a protein or peptide with a given length + in amino acids. This component has no associated mechanism to generate + species or reactions, but can be used to represent enzymes, transcription + factors, structural proteins, or any other protein molecules. + + Parameters + ---------- + name : str + Name of the protein. + length : int, default=0 + Length of the protein in number of amino acids. + attributes : list of str, optional + List of attribute tags to associate with the protein species. Common + attributes include degradation tags (e.g., 'ssrAtagged') or functional + properties (e.g., 'fluorescent'). + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + species : Species + The protein species object with material_type='protein'. + + See Also + -------- + DNA : DNA sequence component. + RNA : RNA sequence component. + Enzyme : Enzymatic protein component. + Component : Base class for biomolecular components. + + Examples + -------- + Create a simple protein: + + >>> protein = bcp.Protein(name='GFP', length=238) + >>> protein.get_species() + protein_GFP + + Create a protein with degradation tag: + + >>> protein = bcp.Protein( + ... name='LacI', + ... length=360, + ... attributes=['ssrAtagged'] + ... ) """ def __init__(self, name: str, length=0, attributes=None, **kwargs): - """Initialize a Protein object to store Protein related information. - - :param name: name of the protein - :param length: length of the protein in number of amino acids - :param attributes: Species attribute - :param kwargs: pass into the parent's (Component) initializer - - """ self.species = self.set_species( name, material_type='protein', attributes=attributes ) @@ -100,23 +252,121 @@ def __init__(self, name: str, length=0, attributes=None, **kwargs): super().__init__(name=name, **kwargs) def get_species(self) -> Species: + """Get the protein species. + + Returns + ------- + Species + The protein species object with material_type='protein'. + + """ return self.species def update_species(self) -> List[Species]: + """Generate species associated with the protein component. + + Returns + ------- + list of Species + List containing only the protein species itself, as Protein + has no associated mechanism to produce additional species. + + """ species = [self.get_species()] return species def update_reactions(self) -> List: + """Generate reactions associated with the protein component. + + Returns + ------- + list + Empty list, as Protein has no associated mechanism. + + """ return [] class Metabolite(Component): - """Metabolic compounded that is produced, utilized, or degraded. - - Notes: + """Metabolic compound that can be produced, utilized, or degraded. + + A `Metabolite` component represents a metabolic compound that + participates in biochemical pathways. It can have precursors (species + that are converted into this metabolite) and products (species that this + metabolite is converted into). The component uses a 'metabolic_pathway' + mechanism to generate production and degradation reactions. + + Parameters + ---------- + name : str + Name of the metabolite. + attributes : list of str, optional + List of attribute tags to associate with the metabolite species. + precursors : list of Species, str, Component, or None, optional + List of chemical species that are directly transformed into this + metabolite via the production mechanism. None represents + constitutive production (production from nothing). + products : list of Species, str, Component, or None, optional + List of chemical species produced from this metabolite via the + degradation mechanism. None represents total degradation + (degradation to nothing). + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + species : Species + The metabolite species object with material_type='metabolite'. + precursors : list of Species or None + List of precursor species. None values represent constitutive + production. + products : list of Species or None + List of product species. None values represent total degradation. + + See Also + -------- + Enzyme : Enzymatic component for catalysis. + Component : Base class for biomolecular components. + + Notes ----- - Metabolites look for 'metabolic_pathway' mechanism, but will not throw - an error if it is not found. + The Metabolite component looks for a 'metabolic_pathway' mechanism but + will not throw an error if it is not found. If the mechanism is present: + + - Production reactions are generated from precursors to the metabolite + - Degradation reactions are generated from the metabolite to products + + None is a valid precursor/product representing constitutive + production/degradation. + + Examples + -------- + Create a metabolite with constitutive production and degradation: + + >>> atp = bcp.Metabolite( + ... name='ATP', + ... precursors=[None], + ... products=[None] + ... ) + + Create a metabolite with specific precursor and product: + + >>> adp = bcp.Metabolite( + ... name='ADP', + ... precursors=['ATP'], + ... products=['AMP'] + ... ) + + Use with a mixture and metabolic pathway mechanism: + + >>> from biocrnpyler.mechanisms import OneStepPathway + >>> mixture = bcp.Mixture( + ... components=[atp], + ... mechanisms={'metabolic_pathway': OneStepPathway()}, + ... parameters={'k': 0.1} + ... ) + >>> crn = mixture.compile_crn() """ @@ -128,17 +378,6 @@ def __init__( products=None, **kwargs, ): - """Initialize and store Metabolite related information. - - :param name: name of the protein - :param attributes: Species attribute - :param precursors: list of chemical species directly transformed - into this metabolite via the production mechanism - :param products: list of chemical species produced from this - metabolite via the degradation mechanism - :param kwargs: pass into the parent's (Component) initializer - - """ self.species = self.set_species( name, material_type='metabolite', attributes=attributes ) @@ -166,9 +405,31 @@ def __init__( Component.__init__(self=self, name=name, **kwargs) def get_species(self) -> Species: + """Get the metabolite species. + + Returns + ------- + Species + The metabolite species object with material_type='metabolite'. + + """ return self.species def update_species(self) -> List[Species]: + """Use 'metabolic_pathway' mechanism to generate species. + + Uses the 'metabolic_pathway' mechanism (if present) to generate + species for production reactions (from precursors to metabolite) and + degradation reactions (from metabolite to products). + + Returns + ------- + list of Species + List of species including the metabolite itself and any additional + species generated by the 'metabolic_pathway' mechanism. If no + mechanism is present, returns only the metabolite species. + + """ species = [self.get_species()] mech_pathway = self.get_mechanism( 'metabolic_pathway', optional_mechanism=True @@ -192,6 +453,19 @@ def update_species(self) -> List[Species]: return species def update_reactions(self) -> List: + """Use 'metabolic_pathway' mechanism to generate reactions. + + Uses the 'metabolic_pathway' mechanism (if present) to generate + production reactions (from precursors to metabolite) and degradation + reactions (from metabolite to products). + + Returns + ------- + list of Reaction + List of reactions including production and degradation pathways. + If no mechanism is present, returns an empty list. + + """ reactions = [] mech_pathway = self.get_mechanism( 'metabolic_pathway', optional_mechanism=True @@ -216,10 +490,81 @@ def update_reactions(self) -> List: class ChemicalComplex(Component): - """Two or more molecules bound together into a complex. + """Complex formed by binding of two or more molecular species. + + A `ChemicalComplex` component represents a molecular complex formed when + two or more species bind together. The complex automatically inherits + attributes from its constituent species. The component uses a 'binding' + mechanism to generate binding and unbinding reactions. + + Parameters + ---------- + species : list of Species, str, or Component + List of species that form the complex. Must contain at least two + elements. Each element can be a `Species` object, string name, or + `Component` with an associated species. + name : str, optional + Name of the complex. If None, a name is automatically generated + from the constituent species names. + material_type : str, default='complex' + Material type identifier for the complex species. Can be customized + for specific complex types. + attributes : list of str, optional + List of attribute tags to associate with the complex species. The + complex also inherits attributes from its constituent species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + species : Complex + The complex species object created from the constituent species. + internal_species : list of Species + List of individual species that make up the complex. + + See Also + -------- + Component : Base class for biomolecular components. + Species : Chemical species representation. + Complex : Species subclass for molecular complexes. + + Notes + ----- + The ChemicalComplex component uses a 'binding' mechanism which must be + provided by the containing mixture. The binding mechanism generates: + + - Forward binding reactions (species --> complex) + - Reverse unbinding reactions (complex --> species) + + The first species in the list is treated as the 'bindee' and remaining + species are treated as 'binders' in the binding mechanism. + + Examples + -------- + Create a simple protein-DNA complex: + + >>> complex = bcp.ChemicalComplex( + ... species=['TF_protein', 'DNA_promoter'], + ... name='TF_bound' + ... ) + + Create an enzyme-substrate complex: - A complex forms when two or more species bind together Complexes - inherit the attributes of their species. + >>> complex = bcp.ChemicalComplex( + ... species=['protein_E', 'S'], + ... name='ES_complex' + ... ) + + Use with a mixture and binding mechanism: + + >>> from biocrnpyler.mechanisms import One_Step_Binding + >>> mixture = bcp.Mixture( + ... components=[complex], + ... mechanisms={'binding': One_Step_Binding()}, + ... parameters={'kb': 1.0, 'ku': 0.1} + ... ) + >>> crn = mixture.compile_crn() """ @@ -231,15 +576,6 @@ def __init__( attributes=None, **kwargs, ): - """Initialize and store ChemicalComplex related information. - - :param species: list of species inside a complex - :param name: name of the complex - :param material_type: option to rename the material_type, - default: 'complex' - :param attributes: Species attribute - :param kwargs: pass into the parent's (Component) initializer - """ if not isinstance(species, list) or len(species) < 2: raise ValueError( f"Invalid Species {species}. Species must be a list of " @@ -265,9 +601,31 @@ def __init__( Component.__init__(self=self, name=name, **kwargs) def get_species(self) -> List[Species]: + """Get the complex species. + + Returns + ------- + Complex + The complex species object containing all constituent species. + + """ return self.species def update_species(self) -> List[Species]: + """Use 'binding' mechanism to generate species for binding reactions. + + Uses the 'binding' mechanism to generate all species needed for + binding and unbinding reactions, including the individual species + and the complex. + + Returns + ------- + list of Species + List of all species generated by the binding mechanism, + typically including the constituent species and the complex + species. + + """ mech_b = self.get_mechanism('binding') bindee = self.internal_species[0] binder = self.internal_species[1:] @@ -281,6 +639,18 @@ def update_species(self) -> List[Species]: return species def update_reactions(self) -> List[Reaction]: + """Use 'binding' mechanism to generate binding/unbinding reactions. + + Uses the 'binding' mechanism to generate reactions for complex + formation (binding) and dissociation (unbinding). + + Returns + ------- + list of Reaction + List of reactions generated by the binding mechanism, typically + including forward binding and reverse unbinding reactions. + + """ mech_b = self.get_mechanism('binding') bindee = self.internal_species[0] binder = self.internal_species[1:] @@ -295,17 +665,96 @@ def update_reactions(self) -> List[Reaction]: class Enzyme(Component): - """A class to represent enzymes with multiple substrates and products. + """Enzyme that catalyzes conversion of substrates to products. + + An `Enzyme` component represents an enzyme that catalyzes the conversion + of one or more substrates into one or more products. The enzyme itself + is not consumed in the reaction. This component uses a 'catalysis' + mechanism to generate the appropriate chemical reactions. + + Parameters + ---------- + enzyme : Species, str, or Component + The enzyme species that catalyzes the reaction. Can be a `Species` + object, a string name (creates new protein Species), or a + `Component` with an associated species. + substrates : list of Species, str, or Component + List of substrate species that are consumed by the enzymatic + reaction. Each element can be a `Species` object, string name, or + `Component`. + products : list of Species, str, or Component + List of product species that are produced by the enzymatic + reaction. Each element can be a `Species` object, string name, or + `Component`. + attributes : list of str, optional + List of attribute tags to associate with the enzyme species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + enzyme : Species + The enzyme species object. + substrates : list of Species + List of substrate species objects. + products : list of Species + List of product species objects. + + See Also + -------- + Component : Base class for biomolecular components. + Metabolite : Component for metabolic compounds. + ChemicalComplex : Component for molecular complexes. + + Notes + ----- + The `Enzyme` component assumes all substrates are converted to all + products in a single enzymatic step: + + S1 + S2 + ... + SN + E --> P1 + P2 + ... + PM + E + + For enzymes that catalyze multiple distinct reactions, create separate + `Enzyme` components with the same internal enzyme species. + + The component uses a mechanism called 'catalysis' which must be + provided by the containing mixture. Common catalysis mechanisms include + Michaelis-Menten kinetics and other enzymatic rate laws. + + Examples + -------- + Create a simple enzyme that converts substrate S to product P: + + >>> enzyme = bcp.Enzyme( + ... enzyme='E', + ... substrates=['S'], + ... products=['P'] + ... ) + >>> enzyme.get_species() + protein_E + + Create an enzyme with multiple substrates and products: + + >>> enzyme = bcp.Enzyme( + ... enzyme='Kinase', + ... substrates=['ATP', 'Protein'], + ... products=['ADP', 'Protein_P'] + ... ) - Assumes the enzyme converts all substrates to a all products at once. - For example: S1 + S2 + ... + S_N + E --> P1 + P2 + ... + P_M + E. For - enzymes with multiple enzymatic reactions, create multiple enzyme - components with the same internal species. Uses a mechanism called - 'catalysis'. + Use with a mixture and Michaelis-Menten mechanism: + + >>> from biocrnpyler.mechanisms import MichaelisMenten + >>> mixture = bcp.Mixture( + ... components=[enzyme], + ... mechanisms={'catalysis': MichaelisMenten()}, + ... parameters={'kb': 0.1, 'ku': 0.01, 'kcat': 1.0} + ... ) + >>> crn = mixture.compile_crn() - TODO: implement multiple substrates and multiple products """ + # TODO: implement multiple substrates and multiple products + def __init__( self, enzyme: Union[Species, str, Component], @@ -314,18 +763,6 @@ def __init__( attributes=None, **kwargs, ): - """Initialize and store enzyme related information. - - :param enzyme: name of the enzyme or reference to an Species - or Component - :param substrates: list of (name of the substrate or reference - to a Species or Component) - :param products: list of (name of the product or reference - to a Species or Component) - :param attributes: Species attribute - :param kwargs: pass into the parent's (Component) initializer - - """ self.enzyme = self.set_species( enzyme, material_type='protein', attributes=attributes ) @@ -336,12 +773,33 @@ def __init__( @property def substrates(self) -> List: + """List of substrate species for the enzymatic reaction. + + Returns + ------- + list of Species + + """ return self._substrates @substrates.setter def substrates( self, new_substrates: List[Union[Species, str, Component]] ): + """Set the substrate species list. + + Parameters + ---------- + new_substrates : Species, str, Component, or list + Substrate(s) to set. Can be a single species or a list. Each + element is converted to a `Species` object. + + Notes + ----- + Automatically converts single substrate to a list and converts all + elements to `Species` objects using `set_species`. + + """ if not isinstance(new_substrates, list): new_substrates = [new_substrates] # convert the new substrates to Species @@ -349,19 +807,62 @@ def substrates( @property def products(self) -> List: + """List of product species for the enzymatic reaction. + + Returns + ------- + list of Species + + """ return self._products @products.setter def products(self, new_products: List[Union[Species, str, Component]]): + """Set the product species list. + + Parameters + ---------- + new_products : Species, str, Component, or list + Product(s) to set. Can be a single species or a list. Each + element is converted to a `Species` object. + + Notes + ----- + Automatically converts single product to a list and converts all + elements to `Species` objects using `set_species`. + + """ if not isinstance(new_products, list): new_products = [new_products] # convert the new products to Products self._products = [self.set_species(p) for p in new_products] def get_species(self) -> Species: + """Get the enzyme species. + + Returns + ------- + Species + The enzyme species object that catalyzes the reaction. + + """ return self.enzyme def update_species(self) -> List[Species]: + """Use 'catalysis' mechanism to generate enzymatic species. + + Uses the 'catalysis' mechanism to generate all species needed for + the enzymatic reaction, including enzyme, substrates, products, and + any intermediate complexes. + + Returns + ------- + list of Species + List of all species generated by the catalysis mechanism, + typically including enzyme, substrates, products, and + enzyme-substrate complexes. + + """ mech_cat = self.get_mechanism('catalysis') return mech_cat.update_species( enzyme=self.enzyme, @@ -370,6 +871,19 @@ def update_species(self) -> List[Species]: ) def update_reactions(self) -> List[Reaction]: + """Use 'catalysis' mechanism to generate enzymatic reactions. + + Uses the 'catalysis' mechanism to generate all reactions needed for + the enzymatic conversion of substrates to products. + + Returns + ------- + list of Reaction + List of all reactions generated by the catalysis mechanism, + typically including substrate binding, catalysis, and product + release steps. + + """ mech_cat = self.get_mechanism('catalysis') return mech_cat.update_reactions( enzyme=self.enzyme, diff --git a/biocrnpyler/components/combinatorial_complex.py b/biocrnpyler/components/combinatorial_complex.py index efc0ecac..7bb87e96 100644 --- a/biocrnpyler/components/combinatorial_complex.py +++ b/biocrnpyler/components/combinatorial_complex.py @@ -9,7 +9,157 @@ class CombinatorialComplex(Component): - """Complex of many Species which bind together in many different ways.""" + """Complex formed through combinatorial binding of multiple species. + + A `CombinatorialComplex` component represents a complex that can form + through multiple combinatorial binding pathways. The component + enumerates all possible intermediate complexes and generates binding + reactions between initial states and final states, optionally + constrained by intermediate states and excluded states. Uses a + 'binding' mechanism to generate combinatorial binding reactions. + + Parameters + ---------- + final_states : ComplexSpecies or list of ComplexSpecies + The final complex(es) to be formed. All binding reactions + ultimately lead to these states. + initial_states : list of Species or ComplexSpecies, optional + Starting species that bind together to form final_states. If None, + defaults to all individual species contained within final_states. + intermediate_states : list of ComplexSpecies, optional + Allowed intermediate complexes formed during binding. Restricts + the binding pathway. If None, all possible intermediates are + enumerated. + excluded_states : list of Species or ComplexSpecies, optional + Species or complexes that are NOT allowed to form. If None, no + complexes are excluded. + name : str, optional + Name of the component. If None, automatically generated from + final_states names. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + final_states : list of ComplexSpecies + List of final complex states. + initial_states : list of Species or ComplexSpecies + List of initial binding species. + intermediate_states : list of ComplexSpecies or None + List of allowed intermediate complexes, or None if unrestricted. + excluded_states : list + List of excluded species/complexes. + sub_species : list of Species + All individual species contained in final_states. + combination_dict : dict + Dictionary storing computed binding combinations. + + See Also + -------- + ChemicalComplex : Simple complex of two or more molecules. + Component : Base class for biomolecular components. + ComplexSpecies : Species subclass for molecular complexes. + + Notes + ----- + The combinatorial binding process generates reactions based on the + provided constraints: + + Case 1 - only `final_states` given: all species in final_states bind + combinatorially: + + individual_species <--> all_intermediates <--> final_states + + Case 2 - `final_states` + `initial_states`: binding starts from + specified initial states directly to final states: + + initial_states <--> final_states + + Case 3 - `final_states` + `intermediate_states`: binding restricted to + specified intermediates: + + individual_species <--> intermediate_states <--> final_states + + Case 4 - `final_states` + `initial_states` + `intermediate_states`: both + initial and intermediate constraints applied: + + initial_states <--> intermediate_states <--> final_states + + The component name is automatically generated as a concatenation of + final_states names separated by underscores if not provided. + + Examples + -------- + Example 1: Full combinatorial binding + + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> C = bcp.Species('C') + >>> final = bcp.Complex([A, B, C]) + >>> cc = bcp.CombinatorialComplex( + ... final_states=final, + ... mechanisms={'binding': bcp.One_Step_Binding()}, + ... parameters={'kb': 1e-1, 'ku': 1e-1}) + + Initial states default to [A, B, C]. All intermediates + [A, B], [A, C], [B, C] are enumerated, resulting in 6 reversible + reactions: + + 1. A + B <--> Complex([A, B]) + 2. A + C <--> Complex([A, C]) + 3. B + C <--> Complex([B, C]) + 4. Complex([A, B]) + C <--> Complex([A, B, C]) + 5. Complex([A, C]) + B <--> Complex([A, B, C]) + 6. Complex([B, C]) + A <--> Complex([A, B, C]) + + Example 2: Constrained initial states + + >>> initial = [bcp.Complex([A, B]), bcp.Complex([A, C])] + >>> cc = bcp.CombinatorialComplex( + ... final_states=final, initial_states=initial, + ... mechanisms={'binding': bcp.One_Step_Binding()}, + ... parameters={'kb': 1e-1, 'ku': 1e-1}) + + Results in 2 reactions: + + 1. Complex([A, B]) + C <--> Complex([A, B, C]) + 2. Complex([A, C]) + B <--> Complex([A, B, C]) + + Example 3: Restricted intermediate states + + >>> inter = [bcp.Complex([A, B]), bcp.Complex([A, C])] + >>> cc = bcp.CombinatorialComplex( + ... final_states=final, intermediate_states=inter, + ... mechanisms={'binding': bcp.One_Step_Binding()}, + ... parameters={'kb': 1e-1, 'ku': 1e-1}) + + Results in 4 reactions: + + 1. A + B <--> Complex([A, B]) + 2. A + C <--> Complex([A, C]) + 3. Complex([A, B]) + C <--> Complex([A, B, C]) + 4. Complex([A, C]) + B <--> Complex([A, B, C]) + + Example 4: Multiple final states with homodimers + + >>> final = [bcp.Complex([A, A, B]), bcp.Complex([A, B, B])] + >>> cc = bcp.CombinatorialComplex( + ... final_states=final, + ... mechanisms={'binding': bcp.One_Step_Binding()}, + ... parameters={'kb': 1e-1, 'ku': 1e-1}) + + Results in 7 reactions including homodimer formation: + + 1. A + A <--> Complex([A, A]) + 2. Complex([A, A]) + B <--> Complex([A, A, B]) + 3. B + B <--> Complex([B, B]) + 4. Complex([B, B]) + A <--> Complex([A, B, B]) + 5. A + B <--> Complex([A, B]) + 6. Complex([A, B]) + A <--> Complex([A, A, B]) + 7. Complex([A, B]) + B <--> Complex([A, B, B]) + + """ def __init__( self, @@ -18,108 +168,12 @@ def __init__( intermediate_states=None, excluded_states=None, name=None, - **keywords, + **kwargs, ): - """Initialize combinatorial complex. - - Binding reactions will be generated to form all the ComplexSpecies in - final_states from all the species in initial_states (or, if - initial_states is None, from all the individual species inside each - ComplexSpecies). Intermediate states restricts the binding reactions - to only form species in this list. Excluded states are not allowed to - be reactants or products. At a high level this generates the - following reactions: - - If just final_states are given: - final_states_internal_species - <-[Combinatorial Binding]-> final_states - - if initial_states are given: - intial_states <-[Combinatorial Binding]-> final_states - - if intermediate_states are given: - final_states_internal_species - <-[Combinatorial Binding]-> intermediate_states - <-[Combinatorial Binding]-> final_states - - if initial_states and intermediate_states are given: - intial_states <-[Combinatorial Binding]-> - intermediate_states <-[Combinatorial Binding]-> final_states - - - :param final_states: a single ComplexSpecies or a list of - ComplexSpecies. - :param initial_states: a list of initial Species which are bound - together to form the ComplexSpecies in final_states. If None, - defaults to the members of the ComplexSpecies in final_states. - :param intermediate_states: a list of intermediate ComplexSpecies - formed when converting initial_states to final_states. If None, - all possible intermediate ComplexSpecies are enumerated. - :param excluded_states: a list of ComplexSpecies which are NOT - allowed to form when converting initial states to final states. - If None, no ComplexSpecies are excluded. - - - Example 1: - final_states=ComplexSpecies([A, B, C]) - initial_states=None - intermediate_states=None - - initial_states will default to A, B, C. All intermediate states - [A, B], [A, C], [B, C] will be enumerated. - - This results in the 6 reversible reactions: - 1. A + B <--> Complex([A, B]) - 2. A + C <--> Complex([A, C]) - 3. B + C <--> Complex([B, C]) - 4. Complex([A, B]) + C <--> Complex([A, B, C]) - 5. Complex([A, C]) + B <--> Complex([A, B, C]) - 6. Complex([B, C]) + A <--> Complex([A, B, C]) - - Example 2: - final_states=ComplexSpecies([A, B, C]) - initial_states=[Complex([A, B]), Complex([A, C])] - intermediate_states=None - - This results in the reactions: - 1. Complex([A, B]) + C <--> Complex([A, B, C]) - 2. Complex([A, C]) + B <--> Complex([A, B, C]) - - Example 3: - final_states=ComplexSpecies([A, B, C]) - initial_states=None, - intermediate_states=[Complex([A, B]), Complex([A, C])]) - - This results in reactions: - 1. A + B <--> Complex([A, B]) - 2. A + C <--> Complex([A, C]) - 3. Complex([A, B]) + C <--> Complex([A, B, C]) - 4. Complex([A, C]) + B <--> Complex([A, B, C]) - - Example 4: - final_states=[Complex([A, A, B], Complex([A, B, B]))] - initial_states=None - intermediate_states=None - - This results in the reactions: - 1. A + A <--> Complex([A, A]) - 2. Complex([A, A]) + B <--> Complex ([A, A, B]) - 3. B + B <--> Complex([B, B]) - 4. Complex([B, B]) + A <--> Complex ([A, B, B]) - 5. A + B <--> Complex([A, B]) - 6. Complex([A, B]) + A <--> Complex ([A, A, B]) - 7. Complex([A, B]) + B <--> Complex ([A, B, B]) - - """ - # The order these run in is important! - - # 1. set final_states + # The order these run in is important! (TODO: why?) self.final_states = final_states - # 2. set initial_states self.initial_states = initial_states - # 3. set intermidiate_states self.intermediate_states = intermediate_states - # 4. set excluded_states self.excluded_states = excluded_states # used to store combinations of species during update @@ -131,16 +185,43 @@ def __init__( for s in self.final_states: name += s.name + '_' name = name[:-1] - super().__init__(name, **keywords) + super().__init__(name, **kwargs) # Final States stores the end complexes that will be formed @property def final_states(self): + """List of final complex states to be formed. + + Returns + ------- + list of ComplexSpecies + + """ return self._final_states @final_states.setter def final_states(self, final_states): - final_states = list(self.set_species(final_states)) + """Set the final complex states. + + Parameters + ---------- + final_states : ComplexSpecies or list of ComplexSpecies + Final complex(es) to be formed through combinatorial binding. + + Raises + ------ + ValueError + If any element in final_states is not a ComplexSpecies. + + Notes + ----- + Also creates a list of all sub-species (individual species + contained in the complexes) stored in `self.sub_species`. + + """ + final_states = self.set_species(final_states) + if not isinstance(final_states, list): + final_states = [final_states] # all final_states must be ComplexSpecies if not all([isinstance(s, ComplexSpecies) for s in final_states]): @@ -160,15 +241,44 @@ def final_states(self, final_states): # Initial states stores the starting states used in binding reactions @property def initial_states(self): + """List of initial states for binding. + + Returns + ------- + list of Species or ComplexSpecies + """ return self._initial_states @initial_states.setter def initial_states(self, initial_states): + """Set the initial binding states. + + Parameters + ---------- + initial_states : list of Species or ComplexSpecies, optional + Starting species for combinatorial binding. If None, defaults + to all individual species in final_states (sub_species). + + Raises + ------ + ValueError + If any initial state is not contained in sub_species or is not + a ComplexSpecies made from sub_species. + + Notes + ----- + Initial states must either be individual species from the + final_states or ComplexSpecies composed of those species. + + """ # set initial states if initial_states is None: self._initial_states = self.sub_species else: - initial_states = list(self.set_species(initial_states)) + initial_states = self.set_species(initial_states) + if not isinstance(initial_states, list): + initial_states = [initial_states] + for s in initial_states: if not ( s in self.sub_species @@ -190,14 +300,43 @@ def initial_states(self, initial_states): # formed between the intial state and final state @property def intermediate_states(self): + """List of allowed intermediate complexes. + + Returns + ------- + list of ComplexSpecies or None + """ return self._intermediate_states @intermediate_states.setter def intermediate_states(self, intermediate_states): + """Set the allowed intermediate complex states. + + Parameters + ---------- + intermediate_states : list of ComplexSpecies, optional + Allowed intermediate complexes formed between initial and + final states. If None, all possible intermediates are + enumerated. + + Raises + ------ + ValueError + If any intermediate state is not a ComplexSpecies, or if any + contains species not in sub_species. + + Notes + ----- + Restricting intermediate states limits the binding pathways and + can reduce the number of reactions generated. + + """ if intermediate_states is None: self._intermediate_states = None else: - intermediate_states = list(self.set_species(intermediate_states)) + intermediate_states = self.set_species(intermediate_states) + if not isinstance(intermediate_states, list): + intermediate_states = [intermediate_states] # All intermediate_states must be ComplexSpecies or # OrderdedComplexSpecies @@ -229,18 +368,63 @@ def intermediate_states(self, intermediate_states): # being enumerated @property def excluded_states(self): + """list: Species or complexes excluded from enumeration.""" return self._excluded_states @excluded_states.setter def excluded_states(self, excluded_states): + """Set the excluded species and complexes. + + Parameters + ---------- + excluded_states : list of Species or ComplexSpecies, optional + Species or complexes that are NOT allowed to form. If None, + no exclusions are applied (empty list). + + Notes + ----- + Excluded states will not appear as reactants or products in + generated reactions. Useful for preventing unwanted binding + pathways. + + """ if excluded_states is None: self._excluded_states = [] else: - self._excluded_states = list(self.set_species(excluded_states)) + excluded_states = self.set_species(excluded_states) + if not isinstance(excluded_states, list): + excluded_states = [excluded_states] + self._excluded_states = excluded_states def compute_species_to_add(self, s0, sf): - # Compute Species that need to be added to s0 to get the Complex sf + """Compute species needed to convert s0 into complex sf. + + Parameters + ---------- + s0 : Species or ComplexSpecies + Starting species or complex. + sf : ComplexSpecies + Target final complex. + + Returns + ------- + list of Species or None + List of species that need to be added to s0 to form sf. Returns + None if s0 contains species not in sf or more copies of any + species than sf contains. + + Raises + ------ + ValueError + If sf is not a ComplexSpecies. + + Notes + ----- + This method compares the stoichiometry of species in s0 and sf to + determine what needs to be added. If s0 contains more of any + species than sf, or contains species not in sf, None is returned. + """ if not isinstance(sf, ComplexSpecies): raise ValueError(f"sf must be a ComplexSpecies. Recieved {sf}") @@ -293,7 +477,48 @@ def compute_species_to_add(self, s0, sf): return species_to_add def get_combinations_between(self, s0, sf): - """All combinations of Species to create the Complex sf from s0.""" + """Get all binding combinations to form complex sf from s0. + + Enumerates all possible binding orders (permutations) to construct + the final complex sf starting from s0, generating tuples of + (binder, bindee, complex_species) for each binding step. + + Parameters + ---------- + s0 : Species or ComplexSpecies + Starting species or complex. + sf : ComplexSpecies + Target final complex. + + Returns + ------- + list of tuple + List of (binder, bindee, complex_species) tuples representing + all possible binding combinations. Each tuple represents one + binding step. Returns empty list if no combinations are + possible. + + Notes + ----- + The method: + + 1. Computes which species need to be added to s0 to form sf + 2. Generates all permutations of these species (different binding + orders) + 3. For each permutation, creates binding steps: binder + bindee --> + complex + 4. Filters out any combinations involving excluded_states + + Examples + -------- + If s0 = A and sf = Complex([A, B, C]), and no exclusions: + + Returns combinations for different binding orders: + + - Order 1: (B, A, [A,B]), (C, [A,B], [A,B,C]) + - Order 2: (C, A, [A,C]), (B, [A,C], [A,B,C]) + + """ species_to_add = self.compute_species_to_add(s0, sf) if species_to_add is None or len(species_to_add) == 0: @@ -330,6 +555,38 @@ def get_combinations_between(self, s0, sf): return combinations def update_species(self): + """Use 'binding' mechanism to generate combinatorial species. + + Uses the 'binding' mechanism to generate species for all possible + binding combinations between initial_states and final_states, + optionally constrained by intermediate_states and excluding + excluded_states. + + Returns + ------- + list of Species + List of all unique species generated, including initial states, + final states, and all intermediate complexes along binding + pathways. + + Notes + ----- + The method handles two cases: + + With intermediate_states: + + 1. Generate combinations: initial_states --> intermediate_states + 2. Generate combinations: intermediate_states --> final_states + + Without intermediate_states: + + Generate combinations: initial_states --> final_states directly + + Duplicate species are automatically removed from the final list. + The combination_dict is populated during this process for use by + `update_reactions`. + + """ mech_b = self.get_mechanism('binding') species = [] species_added_dict = {} # save which combinations have @@ -420,6 +677,37 @@ def update_species(self): return list(set(species)) def update_reactions(self): + """Use 'binding' mechanism to generate combinatorial reactions. + + Uses the 'binding' mechanism to generate reactions for all possible + binding combinations between initial_states and final_states, + optionally constrained by intermediate_states and excluding + excluded_states. + + Returns + ------- + list of Reaction + List of all binding reactions (forward and reverse) along all + enumerated pathways. + + Notes + ----- + The method handles two cases: + + With intermediate_states: + + 1. Generate reactions: initial_states <--> intermediate_states + 2. Generate reactions: intermediate_states <--> final_states + + Without intermediate_states: + Generate reactions: initial_states <--> final_states directly + + Duplicate reactions are automatically filtered out. The method uses + combination_dict computed by update_species() or computes it if + needed. Reactions are symmetric, so (binder, bindee, complex) and + (bindee, binder, complex) are treated as duplicates. + + """ mech_b = self.get_mechanism('binding') reactions = [] reactions_added_dict = {} # save which combinations have @@ -495,7 +783,7 @@ def update_reactions(self): ) # If there are no intermediate restrictions, compute - # combinations in onestep + # combinations in one step else: for s0 in self.initial_states: for sf in self.final_states: diff --git a/biocrnpyler/components/combinatorial_conformation.py b/biocrnpyler/components/combinatorial_conformation.py index ec44232f..ed49a49f 100644 --- a/biocrnpyler/components/combinatorial_conformation.py +++ b/biocrnpyler/components/combinatorial_conformation.py @@ -12,11 +12,109 @@ class CombinatorialConformation(Component): - """Polymer made up of ordered polymer with internal binding complexes. - - A class to represent a PolymerConformation (made of one unique - OrderedPolymerSpecies) with many internal Complexes which can bind and - unbind in many different ways. + """Polymer conformation with combinatorial internal binding complexes. + + A `CombinatorialConformation` component represents a polymer + conformation (made of one unique OrderedPolymerSpecies) with multiple + internal complexes that can bind and unbind in many different ways. + Unlike `CombinatorialComplex` where individual species are added one at + a time, this component adds groups of species in single steps to form + the appropriate complexes. Uses a 'conformation_change' mechanism. + + Parameters + ---------- + final_states : PolymerConformation or list of PolymerConformation + One or more final polymer conformations to be formed. All must + contain the same unique OrderedPolymerSpecies. + initial_states : list of PolymerConformation, optional + Initial polymer conformations that can bind/unbind to become + final_states. If None or empty, defaults to the bare polymer + without complexes. + intermediate_states : list of PolymerConformation, optional + Allowed intermediate conformations formed when converting + initial_states to final_states. If None, all possible + intermediate conformations are enumerated. + excluded_states : list of PolymerConformation, optional + Polymer conformations that will NOT be formed during enumeration. + If None, no conformations are excluded. + state_part_ids : dict, optional + Dictionary mapping PolymerConformation to string, used to generate + shorter part-ids for conformations. + name : str, optional + Name of the component. If None, uses the internal polymer name. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + final_states : list of PolymerConformation + List of final conformation states. + initial_states : list of PolymerConformation + List of initial conformation states. + intermediate_states : list of PolymerConformation or None + List of allowed intermediate conformations, or None if + unrestricted. + excluded_states : list of PolymerConformation + List of excluded conformations. + internal_polymer : OrderedPolymerSpecies + The unique polymer species common to all conformations. + state_part_ids : dict + Dictionary for custom part-id naming. + combination_dict : dict + Dictionary storing computed conformation changes. + + See Also + -------- + CombinatorialComplex : Combinatorial binding of simple complexes. + PolymerConformation : Species subclass for polymer conformations. + Component : Base class for biomolecular components. + + Notes + ----- + Key differences from `CombinatorialComplex`: + + - Operates on `PolymerConformation` objects instead of simple `Species` + - All conformations must share the same `OrderedPolymerSpecies` + - Adds groups of species simultaneously to form complexes + - Uses 'conformation_change' mechanism instead of 'binding' + + Reaction generation: The component generates conformation change + reactions based on constraints: + + - Without intermediate_states: + initial_states <--> final_states + + - With intermediate_states: + initial_states <--> intermediate_states <--> final_states + + Validation requirements: All conformations must: + + 1. Be PolymerConformation objects + 2. Contain exactly one unique OrderedPolymerSpecies + 3. Have the same internal polymer + + Examples + -------- + Create a simple conformational change system: + + >>> A, B, C, S = (bcp.Species(s) for s in ['A', 'B', 'C', 'S']) + >>> pc = bcp.PolymerConformation(polymer=[A, A, B, C]) + >>> # Form a complex A:B by binding positions 0 and 2 + >>> c1 = bcp.Complex([pc.polymers[0][0], pc.polymers[0][2]]) + >>> pc1 = c1.parent + >>> # Form two complexes: A:B and A:C:S (S is external) + >>> c2 = bcp.Complex([pc1.polymers[0][1], pc1.polymers[0][3], S]) + >>> pc2 = c2.parent + >>> # Create component to enumerate reactions + >>> cc = bcp.CombinatorialConformation( + ... final_states=pc2, + ... parameters={'kf': 1, 'kr': 0.01}) + + Using a Mixture to generate species and reactions: + + >>> mixture = bcp.Mixture(components=[cc]) + >>> crn = mixture.compile_crn() """ @@ -30,40 +128,6 @@ def __init__( name=None, **kwargs, ): - """Initialize combinatorial conformation construct. - - Binding reactions will be generated to form all - PolymerConformations in final_states from all the - PolymerConformations in initial_states. There must be a single, - unique, OrderedPolymerSpecies in all the conformations. - Intermediate states restricts the binding reactions to first form - PolymerConformations in this list. At a high level this generates - the following reactions: - - initial_states <- [Combinatorial Binding] -> final_states - - if intermediate_states are given: - initial_states <- [Combinatorial Binding] -> - intermediate_states <- [Combinatorial Binding] -> final_states - - Unlike CombinatorialComplex where Species are added individual, in - CombinatorialConformation, groups of Species are added in single - steps to produce the appropriate Complexes. - - :param final_states: one or more PolymerConformations. - :param initial_states: a list of initial PolymerConformations - which can bind/unbind to become the final_state - :param intermediate_states: a list of intermediate - PolymerConformations formed when converting initial_states to - final_states. If None, all possible intermediate - PolymerConformations are enumerated. - :param excluded_states: a list of intermediate PolymerConformations - which will not be formed during enumeration. If None, no - intermediates will be excluded. - :param state_part_ids: a dictionary {PolymerConformation : str} - used to generate shorter part-ids for this conformation - - """ if state_part_ids is None: self.state_part_ids = {} else: @@ -81,6 +145,26 @@ def __init__( # Helper function to assert the correct class type def _assert_conformation(self, states, input_name='states'): + """Validate that states are proper PolymerConformations. + + Parameters + ---------- + states : list + List of states to validate. + input_name : str, default='states' + Name of the parameter being validated (for error messages). + + Raises + ------ + ValueError + If states are not PolymerConformations, do not contain exactly + one polymer, or do not share the same OrderedPolymerSpecies. + + Notes + ----- + Sets self.internal_polymer on first call if not already set. + + """ if not all([isinstance(s, PolymerConformation) for s in states]): raise ValueError( f"{input_name} must be a list of PolymerConformations. " @@ -111,17 +195,47 @@ def _assert_conformation(self, states, input_name='states'): ) def get_species(self): + """Get the bare polymer conformation. + + Returns + ------- + PolymerConformation + The internal polymer without any complexes. + + """ return PolymerConformation(polymer=self.internal_polymer) # Getters and setters # Final States stores the end complexes that will be formed @property def final_states(self): + """List of final conformation states. + + Returns + ------- + list of PolymerConformation + + """ return self._final_states @final_states.setter def final_states(self, final_states): - final_states = list(self.set_species(final_states)) + """Set the final conformation states. + + Parameters + ---------- + final_states : PolymerConformation or list of PolymerConformation + Final conformation(s) to be formed. + + Raises + ------ + ValueError + If validation fails (see _assert_conformation). + + """ + final_states = self.set_species(final_states) + if not isinstance(final_states, list): + final_states = [final_states] self._assert_conformation(final_states, 'final_states') self._final_states = final_states @@ -129,17 +243,40 @@ def final_states(self, final_states): # Initial states stores the starting states used in binding reactions @property def initial_states(self): + """List of initial conformation states. + + Returns + ------- + list of PolymerConformation + """ return self._initial_states @initial_states.setter def initial_states(self, initial_states): + """Set the initial conformation states. + + Parameters + ---------- + initial_states : list of PolymerConformation, optional + Initial conformations. If None or empty, defaults to bare + polymer conformation. + + Raises + ------ + ValueError + If validation fails (see _assert_conformation). + + """ # set initial states if initial_states is None or len(initial_states) == 0: self._initial_states = [ PolymerConformation(polymer=self.internal_polymer) ] else: - initial_states = list(self.set_species(initial_states)) + initial_states = self.set_species(initial_states) + if not isinstance(initial_states, list): + initial_states = [initial_states] + # all initial_states must be PolymerConformation self._assert_conformation(initial_states, 'initial_states') @@ -149,14 +286,37 @@ def initial_states(self, initial_states): # between the intial state and final state @property def intermediate_states(self): + """List of allowed intermediates. + + Returns + ------- + list of PolymerConformation or None + + """ return self._intermediate_states @intermediate_states.setter def intermediate_states(self, intermediate_states): + """Set the allowed intermediate conformation states. + + Parameters + ---------- + intermediate_states : list of PolymerConformation, optional + Allowed intermediate conformations. If None, all possible + intermediates are enumerated. + + Raises + ------ + ValueError + If validation fails (see _assert_conformation). + + """ if intermediate_states is None: self._intermediate_states = None else: - intermediate_states = list(self.set_species(intermediate_states)) + intermediate_states = self.set_species(intermediate_states) + if not isinstance(intermediate_states, list): + intermediate_states = [intermediate_states] # All intermediate_states must be PolymerConformations self._assert_conformation( @@ -168,27 +328,76 @@ def intermediate_states(self, intermediate_states): # excluded_states are PolymerConformations which are not allowed to form @property def excluded_states(self): + """List of excluded conformations. + + Returns + ------- + list of PolymerConformation + + """ return self._excluded_states @excluded_states.setter def excluded_states(self, excluded_states): + """Set the excluded conformation states. + + Parameters + ---------- + excluded_states : list of PolymerConformation, optional + Conformations that are NOT allowed to form. If None, no + exclusions (empty list). + + Raises + ------ + ValueError + If validation fails (see _assert_conformation). + + """ if excluded_states is None: self._excluded_states = [] else: # All excluded states must be PolymerConformations - excluded_states = list(self.set_species(excluded_states)) + excluded_states = self.set_species(excluded_states) + if not isinstance(excluded_states, list): + excluded_states = [excluded_states] self._assert_conformation(excluded_states, 'excluded_states') self._excluded_states = excluded_states def compute_species_changes(self, s0, sf): - # print("computing species changes between", s0, "and", sf) - # Compute Species that need to be added to s0 to get the - # PolymerConformation sf Assumes the underlying internal - # polymer is the same Computes a list of moves to go from s0 - # --> sf. Each move produces one of the Complexes in SF + """Compute changes needed to convert conformation s0 into sf. + + Analyzes what species need to be added and which complexes need to + be merged to transform the initial conformation s0 into the final + conformation sf. Assumes both conformations share the same + underlying polymer. + + Parameters + ---------- + s0 : PolymerConformation + Starting conformation. + sf : PolymerConformation + Target final conformation. + + Returns + ------- + tuple of (dict, dict) or False + Returns False if s0 cannot be additively transformed into sf. + Otherwise returns (species_changes, merged_complexes) where: + + - species_changes: dict mapping (complex, positions) to list of + external species to add + - merged_complexes: dict mapping (complex, positions) to list of + complexes from s0 that merge to form sf + + Notes + ----- + Returns False if: + + - s0 has more complexes at any position than sf + - Any complex in sf cannot be formed additively from s0 - # 1. if c0 contains bound locations not in cf, c0 cannot be - # transformed additively into cf + """ + # print("computing species changes between", s0, "and", sf) if any( [ len(s0.get_complexes_at(0, i)) @@ -283,7 +492,39 @@ def compute_species_changes(self, s0, sf): return species_changes, merged_complexes def get_combinations_between(self, s0, sf): - """Returns a list of ???.""" + """Get all conformation change combinations from s0 to sf. + + Enumerates all possible orders of complex formation to transform + conformation s0 into sf, generating tuples representing each step. + + Parameters + ---------- + s0 : PolymerConformation + Starting conformation. + sf : PolymerConformation + Target final conformation. + + Returns + ------- + list of tuple + List of (old_state, species_to_add, new_state) tuples + representing all possible transformation pathways. Each tuple + represents one conformation change step. Returns empty list if + no valid pathways exist. + + Notes + ----- + The method: + + 1. Computes which species/complexes change between s0 and sf + 2. Generates all permutations (different formation orders) + 3. For each permutation, creates conformational change steps + 4. Filters out any combinations involving excluded_states + + Unlike `CombinatorialComplex`, this method adds groups of species + simultaneously to form complete complexes at polymer positions. + + """ # print("geting combinations between", s0, "and", sf) X = self.compute_species_changes(s0, sf) @@ -378,12 +619,47 @@ def get_combinations_between(self, s0, sf): return combinations def _get_part_id(self, state): + """Get part ID for a conformation state. + + Parameters + ---------- + state : PolymerConformation + The conformation state. + + Returns + ------- + str + Custom part ID if state is in state_part_ids, otherwise string + representation of the state. + + """ if state in self.state_part_ids: return self.state_part_ids[state] else: return str(state) def update_species(self): + """Use 'conformation_change' mechanism to generate species. + + Uses the 'conformation_change' mechanism to generate species for + all possible conformation transformations between `initial_states` + and `final_states`, optionally constrained by `intermediate_states` + and excluding `excluded_states`. + + Returns + ------- + list of Species + List of all unique species generated, including + polymer conformations and any additional species involved in + conformation changes. + + Notes + ----- + Duplicate species are automatically removed from the final list. + The `combination_dict` is populated during this process for use by + `update_reactions`. + + """ mech_c = self.get_mechanism('conformation_change') species = [] self.combination_dict = {} # should recompute every updated species @@ -480,6 +756,36 @@ def update_species(self): return list(set(species)) def update_reactions(self): + """Use 'conformation_change' mechanism to generate reactions. + + Uses the 'conformation_change' mechanism to generate reactions for + all possible conformation transformations between initial_states + and final_states, optionally constrained by intermediate_states + and excluding excluded_states. + + Returns + ------- + list of Reaction + List of all conformation change reactions (forward and reverse) + along all enumerated pathways. + + Notes + ----- + The method handles two cases: + + With intermediate_states: + + 1. Generate reactions: initial_states <--> intermediate_states + 2. Generate reactions: intermediate_states <--> final_states + + Without intermediate_states: generate reactions: + initial_states <--> final_states directly. + + Duplicate reactions are automatically filtered out using + `reactions_added_dict`. The method uses `combination_dict` computed by + `update_species` or computes it if needed. + + """ mech_c = self.get_mechanism('conformation_change') reactions = [] # save which combinations have already been added in order to @@ -573,35 +879,124 @@ def update_reactions(self): class CombinatorialConformationPromoter(CombinatorialConformation, Promoter): - """Combinatorial promoter with expressing states. - - A combinatorial conformation with an additional set of states - "expressing_states" which can transcribe/express rna/protein - products. This class merges Promoter and - CombinatorialConformation. - - :param promoter_states: one or more PolymerConformations which are used - by the promoter class. - :param promoter_states_on: True/False if True all promoter_states are - transcribable. If False all states except promoter_states are - transcribable. - :param promoter_location: the index of the monomer in the - PolymerConformation which represents the promoter - :param final_states: one or more PolymerConformations. - :param initial_states: a list of initial PolymerConformations which can - bind/unbind to become the final_state. - :param intermediate_states: a list of intermediate PolymerConformations - formed when converting initial_states to final_states. If None, all - possible intermediate PolymerConformations are enumerated. - :param excluded_states: a list of intermediate PolymerConformations - which will not be formed during enumeration. If None: no intermediates - will be excluded. - :param state_part_ids: a dictionary {PolymerConformation : str} used to - generate shorter part-ids for this conformation - :param activating_complexes: a list of ComplexSpecies which activate - PolymerConformations allowing them to be transcribed. - :param inactivating_complexes: a list of ComplexSpecies which innactive - the PolymerConformation, preventing transcription. + """Combinatorial conformation with transcriptionally active states. + + A `CombinatorialConformationPromoter` combines `CombinatorialConformation` + and `Promoter` functionality, creating a polymer with combinatorial + conformations where certain conformations can transcribe/express + RNA/protein products. Specific conformations can be designated as + transcriptionally active ('on') or inactive ('off'). + + Parameters + ---------- + promoter_states : list of PolymerConformation + Polymer conformations used by the promoter. These states are + designated as either 'on' or 'off' based on promoter_states_on. + promoter_location : int + Index of the monomer in the polymer conformation that represents + the promoter location on the polymer. + promoter_states_on : bool, default=True + If True, all `promoter_states` are transcribable. If False, all + states except `promoter_states` are transcribable. + activating_complexes : list of ComplexSpecies, optional + Complexes that activate polymer conformations for transcription + regardless of promoter_states. + inactivating_complexes : list of ComplexSpecies, optional + Complexes that inactivate polymer conformations, preventing + transcription even if otherwise active. + intermediate_states : list of PolymerConformation, optional + Allowed intermediate conformations (see `CombinatorialConformation`). + final_states : list of PolymerConformation, optional + Final conformations (see `CombinatorialConformation`). + name : str, default='CombinatorialConformationPromoter' + Name of the component. + **kwargs + Additional keyword arguments passed to the parent class constructors. + + Attributes + ---------- + promoter_states : list of PolymerConformation + List of designated promoter states. + promoter_states_on : bool + Whether promoter_states are active or inactive. + promoter_location : int + Polymer position of the promoter. + activating_complexes : list of ComplexSpecies + Complexes that activate transcription. + inactivating_complexes : list of ComplexSpecies + Complexes that prevent transcription. + conformation_species : list + All conformation species (populated by update_species). + + See Also + -------- + CombinatorialConformation : Base class for conformational changes. + Promoter : Base class for transcription initiation. + PolymerConformation : Polymer with internal complexes. + + Notes + ----- + A conformation is transcriptionally active if: + + 1. (conformation in promoter_states AND promoter_states_on=True) OR + (conformation not in promoter_states AND promoter_states_on=False) + 2. OR any activating_complex is present in the conformation + 3. AND no inactivating_complex is present + + If inactivating_complex conflicts with active_state or + active_complex, a warning is issued and transcription is prevented. + + The promoter location determines which DNA species from the polymer is + used for transcription initiation. + + Examples + -------- + Create a promoter (operon) with conformational regulation: + + >>> A, B, F = (bcp.Species(s) for s in ['A', 'B', 'F']) + >>> op = bcp.PolymerConformation(polymer=[B, A, B]) + >>> OF0 = bcp.Complex([op.polymers[0][0], F]).parent # F bound at pos'n 0 + >>> OF1 = bcp.Complex([op.polymers[0][2], F]).parent # F bound at pos'n 2 + >>> OF2 = tbp.Complex([OF1.polymers[0][2], F]).parent # F bound to both + >>> # Looped conformations + >>> L0 = Complex([op.polymers[0][0], op.polymers[0][1], F]).parent + >>> L1 = Complex([op.polymers[0][2], op.polymers[0][1], F]).parent + >>> # Define fully bound looped states + >>> L0F1 = bcp.Complex( + ... [OF1.polymers[0][0], OF1.polymers[0][1], F]).parent + >>> L1F0 = bcp. Complex( + ... [OF0.polymers[0][2], OF0.polymers[0][1], F]).parent + >>> # Create promoter with specific active states + >>> ccp = bcp.CombinatorialConformationPromoter( + ... name="CCP", + ... intermediate_states=[OF0, OF1], + ... final_states=[OF2, L0F1, L1F0], + ... promoter_states=[L0F1, L1F0, L0, L1], # transcribed states + ... promoter_states_on=True, + ... promoter_location=1 + ... ) + + With repression (toggle `promoter_states_on`): + + >>> # Same setup as above, but with promoter_states_on=False + >>> # Now only states NOT in promoter_states will transcribe + >>> ccp = bcp.CombinatorialConformationPromoter( + ... name="CCP", + ... intermediate_states=[OF0, OF1], + ... final_states=[OF2, L0F1, L1F0], + ... promoter_states=[L0F1, L1F0, L0, L1], + ... promoter_states_on=False, + ... promoter_location=1 + ... ) + >>> # Use in a DNAassembly for transcription + >>> assy = bcp.DNAassembly( + ... name="X", dna=op, promoter=ccp, rbs="rbs", protein="X") + >>> mixture = bcp.Mixture( + ... components=[assy], + ... mechanisms=[bcp.SimpleTranscription(), bcp.SimpleTranslation()], + ... parameters={'kf': 1, 'kr': 0.01, 'ktx': 1, 'ktl': 1} + ... ) + >>> crn = mixture.compile_crn() """ @@ -661,19 +1056,68 @@ def __init__( # Promoter class. These can be ON or OFF @property def promoter_states(self): + """List of designated promoter states. + + Returns + ------- + list of PolymerConformation + + """ return self._promoter_states @promoter_states.setter def promoter_states(self, promoter_states): + """Set the promoter conformational states. + + Parameters + ---------- + promoter_states : list of PolymerConformation, optional + Conformations designated as promoter states. If None, empty + list. + + Raises + ------ + ValueError + If validation fails (see _assert_conformation). + + """ if promoter_states is None: self._promoter_states = [] else: # All excluded states must be PolymerConformations - promoter_states = list(self.set_species(promoter_states)) + promoter_states = self.set_species(promoter_states) + if not isinstance(promoter_states, list): + promoter_states = [promoter_states] self._assert_conformation(promoter_states, 'promoter_states') self._promoter_states = promoter_states def update_species(self): + """Generate species for conformation changes and transcription. + + Generates species from both conformational changes (via + CombinatorialConformation) and transcription (via Promoter) for + conformations that are transcriptionally active. + + Returns + ------- + list of Species + List of all unique species including conformation states and + transcription-related species (RNAP complexes, transcripts, + etc.) for active conformations. + + Notes + ----- + For each conformation, determines if it is transcriptionally active + based on: + + - Whether it is in promoter_states (and promoter_states_on setting) + - Presence of activating_complexes + - Absence of inactivating_complexes + + Only active conformations generate transcription species via + Promoter.update_species(). + + """ self.conformation_species = CombinatorialConformation.update_species( self ) @@ -725,6 +1169,30 @@ def update_species(self): return list(set(promoter_species + self.conformation_species)) def update_reactions(self): + """Generate reactions for conformation changes and transcription. + + Generates reactions from both conformational changes (via + CombinatorialConformation) and transcription (via Promoter) for + conformations that are transcriptionally active. + + Returns + ------- + list of Reaction + List of all reactions including conformation change reactions + and transcription reactions (RNAP binding, elongation, etc.) + for active conformations. + + Notes + ----- + For each conformation, determines if it is transcriptionally active + using the same logic as update_species(). Only active + conformations generate transcription reactions via + `Promoter.update_reactions`. + + The component name is temporarily changed to a state-specific name + for each conformation to ensure unique reaction identifiers. + + """ if not hasattr(self, 'conformation_species'): self.update_species diff --git a/biocrnpyler/components/component_enumerator.py b/biocrnpyler/components/component_enumerator.py index 868d1fb3..e7e4f209 100644 --- a/biocrnpyler/components/component_enumerator.py +++ b/biocrnpyler/components/component_enumerator.py @@ -6,22 +6,96 @@ class ComponentEnumerator: - def __init__(self, name: str): - """Class to enumerate components. + """Base class for enumerating new components from existing components. + + A `ComponentEnumerator` creates new components in a process similar to + mechanisms. Component enumerators are used during CRN compilation to + expand or transform components, generating derived components that are + then compiled into species and reactions. + + Parameters + ---------- + name : str + Name identifier for the component enumerator. + + Attributes + ---------- + name : str + The name of the enumerator. + + See Also + -------- + LocalComponentEnumerator : Enumerator for single-component processing. + GlobalComponentEnumerator : Enumerator requiring all mixture components. + Mechanism : Base class for reaction generation. + + Notes + ----- + This is a base class that should be subclassed to implement specific + enumeration behavior. The key method to override is + `enumerate_components`. + + Component enumerators are used during the mixture compilation process + to: + + - Generate derived components (e.g., transcripts from DNA) + - Transform components based on context + - Create component variants or states + + Examples + -------- + Create a custom component enumerator: + + >>> class MyEnumerator(bcp.ComponentEnumerator): + ... def __init__(self): + ... super().__init__(name='MyEnumerator') + ... + ... def enumerate_components(self, component=None): + ... # Custom enumeration logic + ... new_components = [] + ... # ... generate new components ... + ... return new_components - A component enumerator's job is to create new components in a - process similar to mechanisms. + """ - """ + def __init__(self, name: str): self.name = name def enumerate_components(self, component=None) -> List: - """Method to enumerate components. - - This will create new components based on the input component - somehow. The child class should implement this. - - :return: empty list + """Enumerate new components from an input component. + + This method creates new components based on the input component. + The base implementation returns an empty list and issues a warning. + Subclasses should override this method to provide specific + enumeration behavior. + + Parameters + ---------- + component : Component, optional + The input component to enumerate from. Can be None for + enumerators that do not require an input component. + + Returns + ------- + list of Component + List of newly enumerated components. Base implementation + returns an empty list. + + Warns + ----- + UserWarning + Issues a warning when the default implementation is called, + indicating that a subclass should override this method. + + See Also + -------- + Component.enumerate_components : Component-level enumeration method. + + Notes + ----- + This method is called during CRN compilation as part of the + component enumeration phase. Subclasses should implement specific + logic to generate derived components. """ warn( @@ -31,15 +105,72 @@ def enumerate_components(self, component=None) -> List: return [] def __repr__(self): + """Return string representation of the enumerator. + + Returns + ------- + str + The name of the enumerator. + + """ return self.name class LocalComponentEnumerator(ComponentEnumerator): - """Class to enumerate local components. - - A component enumerator's job is to create new components in a process - similar to mechanisms. A local component enumerator only cares about - the single component that is passed in + """Component enumerator that operates on individual components. + + A `LocalComponentEnumerator` processes components independently, + creating new components based solely on the properties of a single + input component. This is the most common type of enumerator, used when + enumeration does not require knowledge of other components in the + mixture. + + Parameters + ---------- + name : str + Name identifier for the local component enumerator. + + Attributes + ---------- + name : str + The name of the enumerator (inherited from ComponentEnumerator). + + See Also + -------- + ComponentEnumerator : Base class for component enumerators. + GlobalComponentEnumerator : Enumerator requiring all mixture components. + + Notes + ----- + Local component enumerators are appropriate when: + + - Enumeration depends only on the component being processed + - No cross-component interactions are needed + - Components can be processed independently + + Common examples include: + + - Generating RNA transcripts from DNA components + - Creating protein products from RNA components + - Expanding DNA assemblies into constituent parts + + The `enumerate_components` method receives only the single component + being processed. + + Examples + -------- + Create a custom local enumerator: + + >>> class TranscriptEnumerator(bcp.LocalComponentEnumerator): + ... def __init__(self): + ... super().__init__(name='TranscriptEnumerator') + ... + ... def enumerate_components(self, component=None): + ... if isinstance(component, bcp.DNA): + ... # Create RNA transcript from DNA + ... transcript = bcp.RNA(name=f'{component.name}_transcript') + ... return [transcript] + ... return [] """ @@ -48,13 +179,74 @@ def __init__(self, name: str): class GlobalComponentEnumerator(ComponentEnumerator): - def __init__(self, name: str): - """Class to enumerate global components. + """Component enumerator that operates on all mixture components. + + A `GlobalComponentEnumerator` has access to all components in the + mixture, allowing for complex enumeration that depends on interactions + or relationships between multiple components. This is used when + enumeration decisions require global context. + + Parameters + ---------- + name : str + Name identifier for the global component enumerator. + + Attributes + ---------- + name : str + The name of the enumerator (inherited from ComponentEnumerator). + + See Also + -------- + ComponentEnumerator : Base class for component enumerators. + LocalComponentEnumerator : Enumerator for single-component processing. + + Notes + ----- + Global component enumerators are appropriate when: + + - Enumeration depends on multiple components + - Cross-component interactions must be considered + - Global context or mixture-wide information is needed + + The `enumerate_components` method typically receives all components + in the mixture, allowing the enumerator to make decisions based on the + complete set of components. + + Common examples include: + + - Generating complexes between components + - Creating interaction networks + - Enumerating components based on global constraints + + Performance note: Global enumerators may be more computationally + expensive than local enumerators since they must consider all + components. + + Examples + -------- + Create a custom global enumerator: + + >>> class ComplexEnumerator(bcp.GlobalComponentEnumerator): + ... def __init__(self): + ... super().__init__(name='ComplexEnumerator') + ... + ... def enumerate_components(self, component=None): + ... # Access all components (passed via mixture) + ... # Generate complexes between compatible components + ... new_complexes = [] + ... # ... complex enumeration logic ... + ... return new_complexes + + Use in a mixture: + + >>> enumerator = ComplexEnumerator() + >>> mixture = bcp.Mixture( + ... components=[comp1, comp2, comp3], + ... global_component_enumerators=[enumerator] + ... ) - A component enumerator's job is to create new components in a - process similar to mechanisms. A global component enumerator takes - in every component that is in the mixture. This is for complex - enumeration that cares about other components + """ - """ + def __init__(self, name: str): ComponentEnumerator.__init__(self, name=name) diff --git a/biocrnpyler/components/construct_explorer.py b/biocrnpyler/components/construct_explorer.py index d9a0c991..db41a764 100644 --- a/biocrnpyler/components/construct_explorer.py +++ b/biocrnpyler/components/construct_explorer.py @@ -80,7 +80,7 @@ def check_loop(self): """Check for loops in plasmids (?). If we already went around the plasmid, then what we're checking for is - continuing transcripts or proteins. We don't want to start making new + continuing transcripts or proteins. We do not want to start making new transcripts because we already checked this area for promoters. """ @@ -200,7 +200,7 @@ def terminate_loop(self): ) self.initialize_loop() - # Returns a list of RNAconstructs + # Returns a list of RNA_constructs def return_components(self, component, previously_enumerated=None): return_rnas = [] for rna in self.all_rnas.values(): diff --git a/biocrnpyler/components/dna/assembly.py b/biocrnpyler/components/dna/assembly.py index 18c99c46..8e80da43 100644 --- a/biocrnpyler/components/dna/assembly.py +++ b/biocrnpyler/components/dna/assembly.py @@ -15,7 +15,126 @@ class DNAassembly(DNA): - """A class that contains a Promoter, RBS, transcript, and protein.""" + """High-level representation of a gene expression construct. + + A DNAassembly represents a complete gene expression unit combining a + promoter region, ribosome binding site (RBS), coding sequence, and the + RNA and protein products. This class provides a convenient interface for + modeling the central dogma pathway: DNA --> RNA --> Protein, where the + promoter controls transcription and the RBS controls translation. + + Parameters + ---------- + name : str + Name of the DNA assembly. + dna : DNA, str, or None, optional + The DNA species or name for the assembly. If None, a DNA species + with `name` is created automatically. + promoter : Promoter, str, or None, optional + The promoter component or name controlling transcription. If None, + no transcription occurs. If a string, a default Promoter is created. + transcript : RNA, str, bool, or None, optional + The RNA transcript produced by transcription. If None, an RNA + species with `name` is created. If False, no transcript is created + (used in expression mixtures for direct translation). + rbs : RBS, str, or None, optional + The ribosome binding site component or name controlling translation. + If None, no translation occurs. If a string, a default RBS is + created. + protein : Protein, str, or None, optional + The protein product of translation. If None, a Protein species with + `name` is created automatically. + length : int, optional + Length of the DNA sequence in base pairs. + attributes : list of str, optional + List of attribute tags for the assembly and its species. + mechanisms : dict or list, optional + Custom mechanisms for this assembly, overriding mixture defaults. + compartment : Compartment, optional + The compartment containing this assembly and its products. + parameters : dict, optional + Parameter values specific to this assembly. + initial_concentration : float, optional + Initial concentration of the DNA species. + **kwargs + Additional keyword arguments passed to the parent `DNA` class. + + Attributes + ---------- + dna : Species + The DNA species representing the genetic construct. + promoter : Promoter or None + The promoter component controlling transcription. + rbs : RBS or None + The ribosome binding site controlling translation. + transcript : Species or None + The RNA transcript produced by transcription. + protein : Species or None + The protein product of translation. + + See Also + -------- + DNA : Base class for DNA components. + Promoter : Component representing transcriptional control elements. + RBS : Component representing ribosome binding sites. + RNA : Base class for RNA components. + Protein : Base class for protein components. + + Notes + ----- + The DNAassembly automatically coordinates its sub-components (promoter, + RBS) by propagating updates to mechanisms, parameters, and mixtures. When + mechanisms or parameters are added to the assembly, they are also added + to the promoter and RBS (but never overwrite existing values in those + components). + + The 'transcription' mechanism is used by the promoter to generate the + species and reactions for transcript and the 'translation' mechanism is + used by the RBS to generate the species and reactions for ribosome binding + and protein production. + + For expression mixtures where transcription is bypassed, set + `transcript=False` to enable direct translation from DNA to protein. In + this case, the 'transcription' mechanism will be used to generate the + protein. + + Examples + -------- + Create a simple constitutive gene expression construct: + + >>> # Basic assembly with automatic species creation + >>> gene = bcp.DNAassembly( + ... name='gene_gfp', + ... promoter='pconst', + ... rbs='rbs1' + ... ) + >>> gene.dna + dna_gene_gfp + >>> gene.transcript + rna_gene_gfp + >>> gene.protein + protein_gene_gfp + + Create an assembly with custom species names: + + >>> gene = bcp.DNAassembly( + ... name='gene_reporter', + ... promoter='p_lac', + ... rbs='rbs_strong', + ... transcript='mRNA_gfp', + ... protein='protein_gfp' + ... ) + + Create an expression construct (no transcript): + + >>> gene = bcp.DNAassembly( + ... name='gene_direct', + ... promoter='p_const', + ... transcript=False, + ... protein='protein_x' + ... ) + + """ def __init__( self, @@ -31,26 +150,11 @@ def __init__( compartment=None, parameters=None, initial_concentration=None, - **keywords, + **kwargs, ): - """Initialize a DNA assembly. - - Note: If transcript is None and protein is not None, the - DNAassembly will use its transcription mechanisms to produce the - protein. This is used by Expression Mixtures. - - :param name: name of the DNA assembly - :param dna: - :param promoter: - :param transcript: - :param rbs: - :param protein: - :param length: - :param attributes: - :param mechanisms: - :param parameters: - :param initial_concentration: - :param keywords: passed into the parent object (DNA) + """Initialize a DNAassembly. + + See class docstring for parameter descriptions. """ self.promoter = None @@ -68,7 +172,7 @@ def __init__( initial_concentration=initial_concentration, attributes=attributes, compartment=compartment, - **keywords, + **kwargs, ) self.update_dna(dna, attributes=attributes) @@ -80,13 +184,27 @@ def __init__( self.update_rbs(rbs, transcript=self.transcript, protein=self.protein) def get_species(self): + """Get the primary DNA species of this assembly. + + Returns + ------- + Species + The DNA species representing this genetic construct. + + """ return self.dna def set_mixture(self, mixture: Mixture) -> None: - """Set the mixture the Component is in. + """Set the mixture containing this component and its sub-components. + + Also propagates the mixture reference to the promoter and RBS + components if they exist. + + Parameters + ---------- + mixture : Mixture + The mixture object that contains this assembly. - :param mixture: reference to a Mixture instance - :return: None """ self.mixture = mixture if self.promoter is not None: @@ -95,11 +213,25 @@ def set_mixture(self, mixture: Mixture) -> None: self.rbs.set_mixture(mixture) def update_dna(self, dna: Union[None, DNA, str], attributes=None) -> None: - """Sets up the dna attribute with a valid DNA instance. + """Set or update the DNA species for this assembly. + + Creates a DNA species from the provided input and updates the DNA + references in the promoter and RBS components if they exist. + + Parameters + ---------- + dna : DNA, str, or None + The DNA component, species name, or None. If None, creates a DNA + species using the assembly's name. If a string, creates a new DNA + species with that name. If a DNA object, uses it directly. + attributes : list of str, optional + Attribute tags to add to the DNA species. + + Notes + ----- + This method automatically updates the `dna` attribute of the promoter + and RBS components to maintain consistency across the assembly. - :param dna: name of a dna sequence or a DNA instance - :param attributes: Species attribute - :return: None """ if dna is None: self.dna = self.set_species( @@ -124,11 +256,32 @@ def update_dna(self, dna: Union[None, DNA, str], attributes=None) -> None: def update_transcript( self, transcript: Union[None, RNA, str, bool], attributes=None ) -> None: - """Sets up the transcript attribute with a valid RNA instance. + """Set or update the RNA transcript for this assembly. + + Creates an RNA species from the provided input and updates the + transcript references in the promoter and RBS components if they + exist. + + Parameters + ---------- + transcript : RNA, str, bool, or None + The RNA component, species name, False, or None. If None, creates + an RNA species using the assembly's name. If a string, creates a + new RNA species with that name. If an RNA object, uses it + directly. If False, sets transcript to None (used for expression + mixtures without transcription). + attributes : list of str, optional + Attribute tags to add to the RNA species. + + Notes + ----- + Setting `transcript=False` is used in expression mixtures where + translation occurs directly from DNA without an explicit RNA + intermediate. + + This method automatically updates the `transcript` attribute of the + promoter and RBS components to maintain consistency. - :param transcript: name of a RNA transcript or RNA instance - :param attributes: Species attribute - :return: None """ if transcript is None: self.transcript = self.set_species( @@ -157,11 +310,27 @@ def update_transcript( def update_protein( self, protein: Union[None, Protein, str], attributes=None ) -> None: - """Sets up the protein attribute with a valid Protein instance. + """Set or update the protein product for this assembly. + + Creates a Protein species from the provided input and updates the + protein references in the promoter and RBS components if they exist. + + Parameters + ---------- + protein : Protein, str, or None + The Protein component, species name, or None. If None, creates a + Protein species using the assembly's name. If a string, creates a + new Protein species with that name. If a Protein object, uses it + directly. + attributes : list of str, optional + Attribute tags to add to the Protein species. + + Notes + ----- + This method automatically updates the `protein` attribute of the + promoter and RBS components to maintain consistency across the + assembly. - :param protein: name of a protein or Protein instance - :param attributes: Species attribute - :return: None """ if protein is None: self.protein = self.set_species( @@ -189,12 +358,34 @@ def update_promoter( transcript: RNA = None, protein: Protein = None, ) -> None: - """Sets up the promoter attribute with a valid Promoter instance. + """Set or update the promoter component for this assembly. + + Creates a Promoter component from the provided input and propagates + the assembly's parameters, mixture, and mechanisms to the promoter. + + Parameters + ---------- + promoter : Promoter, str, or None + The Promoter component, promoter name, or None. If None, no + promoter is created. If a string, creates a default Promoter with + that name using `Promoter.from_promoter`. If a Promoter object, + uses it directly. + transcript : RNA, optional + The RNA transcript to associate with the promoter. If provided, + updates the assembly's transcript before creating the promoter. + protein : Protein, optional + The protein product to associate with the promoter (used for some + regulatory mechanisms). + + Notes + ----- + This method automatically: + + - Propagates the assembly's parameter database to the promoter + - Sets the promoter's mixture reference + - Adds the assembly's mechanisms to the promoter (without overwriting + existing promoter mechanisms) - :param promoter: name of a promoter or Promoter instance - :param transcript: reference to the RNA transcript - :param protein: - :return: None """ if transcript is not None: self.update_transcript(transcript) @@ -225,12 +416,33 @@ def update_rbs( transcript: RNA = None, protein: Protein = None, ) -> None: - """Sets up the rbs attribute with a valid RBS instance. + """Set or update the ribosome binding site component. + + Creates an RBS component from the provided input and propagates the + assembly's parameters, mixture, and mechanisms to the RBS. + + Parameters + ---------- + rbs : RBS, str, or None + The RBS component, RBS name, or None. If None, no RBS is created. + If a string, creates a default RBS with that name using + `RBS.from_rbs`. If an RBS object, uses it directly. + transcript : RNA, optional + The RNA transcript containing the RBS. If provided, updates the + assembly's transcript before creating the RBS. + protein : Protein, optional + The protein product of translation. If provided, updates the + assembly's protein before creating the RBS. + + Notes + ----- + This method automatically: + + - Propagates the assembly's parameter database to the RBS + - Sets the RBS's mixture reference + - Adds the assembly's mechanisms to the RBS (without overwriting + existing RBS mechanisms) - :param rbs: name of the ribosome binding site or RBS instance - :param transcript: RNA that contains the ribosome binding site - :param protein: protein that RNA contains - :return: None """ if protein is not None: self.update_protein(protein) @@ -257,10 +469,25 @@ def update_rbs( self.rbs.add_mechanisms(self.mechanisms, optional_mechanism=True) def update_species(self) -> List[Species]: - """Collects the list of Species that a DNAassemlby instance holds. + """Generate all species associated with this assembly. + + Collects species from the DNA, promoter, and RBS components during + CRN compilation. + + Returns + ------- + list of Species + List containing the DNA species and all species generated by the + promoter and RBS components. + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn` to collect all chemical species generated by + this assembly. - :return: list of Species that a DNAassemlby instance holds """ + # :return: list of Species that a DNAassemlby instance holds species = [] species.append(self.dna) if self.promoter is not None: @@ -280,10 +507,26 @@ def update_species(self) -> List[Species]: return species def update_reactions(self) -> List[Reaction]: - """Collects the list of Reactions that a DNAassemlby instance holds. + """Generate all reactions associated with this assembly. + + Collects reactions from the promoter and RBS components during CRN + compilation. + + Returns + ------- + list of Reaction + List of all reactions generated by the promoter and RBS + components, including transcription, translation, and regulatory + reactions. + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn` to collect all chemical reactions generated by + this assembly. - :return: list of Reactions that a DNAassemlby instance holds. """ + # :return: list of Reactions that a DNAassemlby instance holds. reactions = [] if self.promoter is not None: reactions += self.promoter.update_reactions() @@ -307,12 +550,29 @@ def update_parameters( parameters: ParameterDatabase = None, overwrite_parameters: bool = True, ) -> None: - """Updates the parameters stored in dna, promoter and rbs. + """Update parameters for the assembly and its sub-components. + + Propagates parameter updates to the DNA assembly itself and to the + promoter and RBS components if they exist. + + Parameters + ---------- + parameter_file : str, optional + Path to a CSV or TSV parameter file to load. + parameters : ParameterDatabase, optional + ParameterDatabase object to merge with the assembly's parameters. + overwrite_parameters : bool, default=True + If True, new parameter values overwrite existing ones. If False, + existing parameters are preserved. + + Notes + ----- + This method calls `update_parameters` on: + + 1. The parent DNA class (updating the DNA's parameters) + 2. The promoter component (if it exists) + 3. The RBS component (if it exists) - :param parameter_file: valid parameter file - :param parameters: a parameter database instance - :param overwrite_parameters: whether to overwrite existing parameters - :return: None """ DNA.update_parameters( self=self, @@ -342,16 +602,31 @@ def add_mechanism( overwrite: bool = False, optional_mechanism: bool = False, ) -> None: - """Adds mechanism to the Component mechanism dictionary. - - DNA_assembly also adds the mechanisms to its promoter and rbs - (but never overwrites them!). - - :param mechanism: reference to a Mechanism instance - :param mech_type: type of mechanism - :param overwrite: whether to overwrite the mechanism in Component - :param optional_mechanism: - :return: None + """Add a mechanism to the assembly and its sub-components. + + Adds the mechanism to the assembly's mechanism dictionary and + propagates it to the promoter and RBS components without overwriting + their existing mechanisms. + + Parameters + ---------- + mechanism : Mechanism + The mechanism object to add. + mech_type : str, optional + The mechanism type key. If None, uses the mechanism's + `mechanism_type` attribute. + overwrite : bool, default=False + If True, overwrites existing mechanisms with the same type in the + assembly. If False, raises ValueError for duplicate types. + optional_mechanism : bool, default=False + If True, suppresses ValueError when a mechanism key conflict + occurs in the assembly and `overwrite` is False. + + Notes + ----- + The mechanism is always added to the promoter and RBS with + `optional_mechanism=True`, meaning it will never overwrite existing + mechanisms in those components even if `overwrite=True`. """ Component.add_mechanism( diff --git a/biocrnpyler/components/dna/cds.py b/biocrnpyler/components/dna/cds.py index 85041909..f75202d1 100644 --- a/biocrnpyler/components/dna/cds.py +++ b/biocrnpyler/components/dna/cds.py @@ -7,9 +7,76 @@ class CDS(DNA_part): + """Coding sequence component representing a protein-coding region. + + A CDS (coding sequence) represents a contiguous DNA sequence that codes + for a protein product. This component stores the protein species + associated with the coding region but does not directly generate + translation reactions. Translation is typically handled by an RBS + component that acts on the transcript containing this CDS. + + Parameters + ---------- + name : str + Name of the coding sequence. + protein : Protein, str, Component, or None, optional + The protein product encoded by this CDS. Can be a `Species` object, + string name (creates a protein Species), `Component` with an + associated species, or None (creates protein Species using CDS name). + no_stop_codons : list, optional + List of regions without stop codons, used for sequence validation or + special handling of coding sequences. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + name : str + Name of the coding sequence. + protein : Species + The protein species encoded by this CDS. + + See Also + -------- + RBS : Component that controls translation of proteins. + DNAassembly : Container for CDS and other genetic parts. + DNA_part : Base class for DNA component parts. + + Notes + ----- + The CDS component itself does not generate any reactions during CRN + compilation. It serves primarily as a data structure to associate a + coding sequence with its protein product. The actual translation + reactions are generated by RBS components that reference the transcript + containing this CDS. + + Examples + -------- + Create a CDS with automatic protein naming: + + >>> cds = bcp.CDS(name='gfp') + >>> cds.protein + protein_gfp + + Create a CDS with explicit protein name: + + >>> cds = bcp.CDS( + ... name='cds_reporter', + ... protein='GFP_protein' + ... ) + + Use a CDS in a DNA assembly: + + >>> assembly = bcp.DNAassembly( + ... name='gene_construct', + ... promoter='pconst', + ... rbs='rbs1', + ... protein=cds + ... ) + + """ + def __init__(self, name, protein=None, no_stop_codons=[], **kwargs): - """A CDS is a sequence of DNA that codes for a protein.""" - self.name = name DNA_part.__init__(self, name, no_stop_codons=no_stop_codons, **kwargs) # TODO use set_species() if protein is None: @@ -20,10 +87,35 @@ def __init__(self, name, protein=None, no_stop_codons=[], **kwargs): self.protein = protein.get_species() def update_species(self): + """Generate species associated with this CDS. + + Returns + ------- + list of Species + List containing only the protein species encoded by this CDS. + + """ return [self.protein] def update_reactions(self): + """Generate reactions associated with this CDS. + + Returns + ------- + list of Reaction + Empty list. CDS components have no associated mechanism. + Translation reactions are generated by the RBS component. + + """ return [] def get_species(self): + """Get the protein species encoded by this CDS. + + Returns + ------- + Species + The protein species associated with this coding sequence. + + """ return self.protein diff --git a/biocrnpyler/components/dna/construct.py b/biocrnpyler/components/dna/construct.py index 94bcd597..aaaba0a0 100644 --- a/biocrnpyler/components/dna/construct.py +++ b/biocrnpyler/components/dna/construct.py @@ -20,6 +20,107 @@ class Construct(Component, OrderedPolymer): + """Base class for ordered genetic constructs with multiple parts. + + A Construct represents an ordered arrangement of genetic parts (promoters, + RBS, coding sequences, terminators, etc.) that form a functional unit. + This class provides the infrastructure for handling complex genetic + constructs with multiple components, their enumeration, and generation + of combinatorial variants. Constructs can be linear or circular and + support both forward and reverse orientations of their constituent parts. + + Parameters + ---------- + parts_list : list of list + List of parts in the format [[part, direction], [part, direction], + ...] where each part must be an OrderedMonomer and direction is + 'forward' or 'reverse'. + name : str, optional + Name of the construct. If None, automatically generated from parts. + circular : bool, default=False + If True, the construct is circular (e.g., plasmid). If False, linear. + mechanisms : dict or list, optional + Custom mechanisms for this construct, overriding mixture defaults. + parameters : dict, optional + Parameter values specific to this construct. + attributes : list of str, optional + List of attribute tags for the construct. + initial_concentration : float, optional + Initial concentration of the construct species. + component_enumerators : list, optional + List of enumerator objects that generate construct variants. + make_dirless_hash : bool, default=True + If True, generates direction-independent hash for construct + comparison. + **kwargs + Additional keyword arguments passed to Component constructor. + + Attributes + ---------- + parts_list : list + Ordered list of parts in the construct. + circular : bool + Whether the construct is circular. + component_enumerators : list + Enumerators for generating construct variants. + out_components : list or None + Cached list of output components from enumeration. + predicted_rnas : list or None + Cached list of predicted RNA species. + predicted_proteins : list or None + Cached list of predicted protein species. + + See Also + -------- + DNA_construct : DNA-specific construct implementation. + RNA_construct : RNA-specific construct implementation. + DNA_part : Base class for individual DNA parts. + OrderedPolymer : Base class for ordered polymer structures. + + Notes + ----- + Constructs support several advanced features: + + - Part enumeration: Automatically generates all functional variants + based on the parts present (e.g., all promoter-RBS combinations) + - Combinatorial complexes: Generates all possible binding states + when regulatory proteins bind to different parts + - Direction-free comparison: Can identify equivalent constructs + regardless of orientation or circular permutation + + The class maintains caches for enumerated components, RNA products, and + protein products to avoid redundant computation. + + Examples + -------- + Create a simple linear construct: + + >>> promoter = bcp.Promoter('ptet') + >>> rbs = bcp.RBS('RBS_standard') + >>> cds = bcp.CDS('GFP') + >>> parts = [[promoter, 'forward'], [rbs, 'forward'], [cds, 'forward']] + >>> construct = bcp.Construct( + ... parts_list=parts, + ... name='gene_circuit', + ... circular=False + ... ) + + Create a circular plasmid: + + >>> ori = bcp.DNA_part('p15A') + >>> terminator = bcp.Terminator('BBa_B0022') + >>> parts = [ + >>> [ori, 'forward'], [promoter, 'forward'], [rbs, 'forward'], + >>> [cds, 'forward'], [terminator, 'forward'] + >>> ] + >>> plasmid = bcp.Construct( + ... parts_list=parts, + ... name='pExpression', + ... circular=True + ... ) + + """ + def __init__( self, parts_list, @@ -33,12 +134,6 @@ def __init__( make_dirless_hash=True, **kwargs, ): - """This represents a bunch of parts in a row. - - A parts list has [[part, direction], [part, direction], ...]. - Each part must be an OrderedMonomer. - - """ if component_enumerators is None: component_enumerators = [] self.component_enumerators = component_enumerators @@ -75,6 +170,48 @@ def parts_list(self): return self.polymer def make_name(self): + """Generate a systematic name for the construct based on its parts. + + Creates a name by concatenating all part names with underscores. + Parts in reverse orientation are suffixed with '_r', and circular + constructs are suffixed with '_o'. + + Returns + ------- + str + The generated construct name. + + Examples + -------- + Linear construct with forward parts: + + >>> promoter = bcp.Promoter('pLac') + >>> rbs = bcp.CDS('RBS1') + >>> cds = bcp.CDS('GFP') + >>> construct = bcp.DNA_construct( + ... [[promoter, 'forward'], [rbs, 'forward'], [cds, 'forward']] + ... ) + >>> construct.make_name() + 'pLac_RBS1_GFP' + + Linear construct with reversed part: + + >>> construct = bcp.DNA_construct( + ... [[promoter, 'forward'], [rbs, 'reverse'], [cds, 'forward']] + ... ) + >>> construct.make_name() + 'pLac_RBS1_r_GFP' + + Circular construct: + + >>> construct = bcp.DNA_construct( + ... [[promoter, 'forward'], [rbs, 'forward'], [cds, 'forward']], + ... circular=True + ... ) + >>> construct.make_name() + 'pLac_RBS1_GFP_o' + + """ output = '' outlst = [] for part in self.parts_list: @@ -88,19 +225,75 @@ def make_name(self): return output def get_part(self, part=None, part_type=None, name=None, index=None): - """Function to get parts from Construct.parts_list. - - One of the 3 keywords must not be None. - - part: an instance of a DNA_part. Searches Construct.parts_list - for a DNA_part with the same type and name. - part_type: a class of DNA_part. For example, Promoter. Searches - Construct.parts_list for a DNA_part with the same type. - name: str. Searches Construct.parts_list for a DNA_part with - the same name - index: int. returns Construct.parts_list[index] + """Find and return a part from the construct by various criteria. + + Searches the construct's parts list for a part matching the given + criteria. Only one search criterion should be provided at a time. + + Parameters + ---------- + part : DNA_part, optional + A specific DNA_part instance to find. Matches both type and name. + part_type : type, optional + Find all parts that are instances of this type (e.g., Promoter). + name : str, optional + Find part(s) with this exact name. + index : int, optional + Return the part at this position in parts_list. + + Returns + ------- + DNA_part, list of DNA_part, or None + Single matching part if exactly one match found. List of parts if + multiple matches found. None if no matches found. + + Raises + ------ + ValueError + If multiple search criteria are provided simultaneously, or if + invalid types are provided for parameters. + + Warns + ----- + UserWarning + If multiple matching parts are found (returns list). + + Notes + ----- + The search is performed with the following priority: + + 1. Index (direct lookup) + 2. Part instance (type and name must match) + 3. Name (string match) + 4. Type (isinstance check) + + Only one search criterion should be provided at a time. + + Examples + -------- + Find a part by name: + + >>> promoter = bcp.Promoter('ptet') + >>> rbs = bcp.RBS('RBS_standard') + >>> cds = bcp.CDS('GFP') + >>> parts = [ + ... [promoter, 'forward'], [rbs, 'forward'], [cds, 'forward'] + ... ] + >>> construct = bcp.Construct( + ... parts_list=parts, + ... name='gene_circuit', + ... circular=False + ... ) + >>> promoter = construct.get_part(name='ptet') + + Get the third part in the construct: + + >>> third_part = construct.get_part(index=2) + + Find all RBS parts: + + >>> all_rbs = construct.get_part(part_type=bcp.RBS) - If nothing is found, returns None. """ if [part, name, index, part_type].count(None) != 3: raise ValueError( @@ -146,12 +339,43 @@ def get_part(self, part=None, part_type=None, name=None, index=None): return matches def reverse(self): - """Reverses everything, without actually changing the DNA. + """Reverse the construct without modifying the underlying DNA. + + Creates a reversed representation of the construct where all parts + are in reverse order and each part's direction is flipped. This is + useful for generating the reverse complement of a construct. + + Returns + ------- + Construct + Returns self after reversing the parts list and flipping + directions. - Also updates the name and stuff, since this is now a different - Construct. + Notes + ----- + This method modifies the construct in place by: + + 1. Reversing the order of parts in the parts_list + 2. Flipping each part's direction (forward <--> reverse) + + The underlying DNA sequence is not modified, only the representation + changes. + + Examples + -------- + Reverse a simple construct: + + >>> promoter = bcp.Promoter('ptet') + >>> gene = bcp.CDS('GFP') + >>> construct = bcp.Construct( + ... [[promoter, 'forward'], [gene, 'forward']]) + >>> construct.reverse() + Construct = GFP_r_pLac_r """ + # Reverses everything, without actually changing the DNA. + # Also updates the name and stored, since this is now a different + # Construct. OrderedPolymer.reverse(self) self.reset_stored_data() self.name = self.make_name() @@ -159,16 +383,81 @@ def reverse(self): return self def get_reversed(self): - """Reversed version of construct without changing the construct.""" + """Create a deep copy of the construct with reversed orientation. + + Returns a new construct that is the reverse complement of this + construct, with all parts in reverse order and flipped directions. + The original construct is not modified. + + Returns + ------- + Construct + A new construct object with reversed parts and directions. + + See Also + -------- + reverse : Reverse the construct in place. + + Examples + -------- + Get reversed version without modifying original: + + >>> promoter = bcp.Promoter('ptet') + >>> cds = bcp.CDS('GFP') + >>> original = bcp.Construct( + ... [[promoter, 'forward'], [cds, 'forward']]) + >>> original.get_reversed() + Construct = GFP_r_ptet_r + + """ outcon = copy.deepcopy(self) outcon.reverse() return outcon def get_circularly_permuted(self, new_first_position): - """Circularly permute a construct. - - Returns a new construct which has the first position changed - to new_first_position. + """Create a circularly permuted version of this construct. + + Returns a new construct where the circular ordering of parts starts + at a different position. Only valid for circular constructs. + + Parameters + ---------- + new_first_position : int + The index of the part that should become the first position in + the new construct. + + Returns + ------- + DNA_construct + A new circular DNA_construct with parts reordered starting from + the specified position. + + Raises + ------ + ValueError + If the construct is linear (circular permutation only applies + to circular constructs). + + Notes + ----- + The parts list is rotated so that `parts_list[new_first_position]` + becomes the new first element, maintaining the circular structure. + + Examples + -------- + Permute a circular plasmid: + + >>> ori = bcp.DNA_part('p15A') + >>> promoter = bcp.Promoter('ptet') + >>> cds = bcp.CDS('GFP') + >>> plasmid = bcp.DNA_construct( + ... [[ori, 'forward'], [promoter, 'forward'], [cds, 'forward']], + ... circular=True + ... ) + >>> plasmid + DNA_construct = p15A_ptet_GFP_o + >>> plasmid.get_circularly_permuted(1) + DNA_construct = ptet_GFP_p15A_o """ if not self.circular: @@ -185,20 +474,73 @@ def get_circularly_permuted(self, new_first_position): ) def set_mixture(self, mixture): + """Set the mixture containing this construct and all its parts. + + Propagates the mixture reference to all parts in the construct, + ensuring they share access to the same parameter database and + mechanisms. + + Parameters + ---------- + mixture : Mixture + The mixture object that contains this construct. + + Notes + ----- + This method ensures that all parts in the construct have access to + the same mixture-level parameters and mechanisms, maintaining + consistency across the entire construct. + + """ self.mixture = mixture for part in self.parts_list: part.set_mixture(mixture) def update_permutation_hash(self): - """Update hash for this DNA construct. + """Update the direction-independent hash for this construct. + + Generates a unique string representation of the construct that is + invariant to direction (forward/reverse) and circular permutations + (for circular constructs). This enables comparison of functionally + equivalent constructs regardless of their representation. + + Notes + ----- + The hash is stored in the `directionless_hash` attribute and is used + for identifying equivalent constructs that differ only in orientation + or circular starting position. + + The hash is computed using the `omnihash` class method, which finds + the most alphabetically ordered representation of the construct. - Update the unique string generated to represent the content of - this dna construct regardless of orientation and rotation. + See Also + -------- + omnihash : Class method that computes the direction and rotation-free + hash. """ self.directionless_hash, _, _ = Construct.omnihash(self) def update_base_species(self, base_name=None, attributes=None): + """Update the base species representation of this construct. + + Sets the `base_species` attribute to a Species object representing + the construct's primary chemical species in the CRN. + + Parameters + ---------- + base_name : str, optional + Name for the base species. If None, uses the construct's name. + attributes : list of str, optional + Attribute tags to add to the species. + + Notes + ----- + The base species serves as the chemical representation of the + construct in CRN compilation, with material_type matching the + construct's material_type (e.g., 'dna' for DNA_constructs). + + """ if base_name is None: self.base_species = self.set_species( self.name, @@ -213,7 +555,30 @@ def update_base_species(self, base_name=None, attributes=None): ) def update_parameters(self, overwrite_parameters=True): - """Update parameters of all parts in the construct.""" + """Update parameters for the construct and all its parts. + + Propagates parameter updates from the construct's parameter database + to all parts in the parts list, ensuring consistent parameters + throughout the construct. + + Parameters + ---------- + overwrite_parameters : bool, default=True + If True, new parameter values overwrite existing ones in the + parts. If False, existing parameters in parts are preserved. + + Notes + ----- + This method: + + 1. Updates the construct's own parameters via the parent Component + class + 2. Propagates these parameters to each part in the construct + + This ensures that all parts have access to the same parameter values + unless explicitly overridden at the part level. + + """ Component.update_parameters( self=self, parameter_database=self.parameter_database ) @@ -230,6 +595,36 @@ def add_mechanism( overwrite=False, optional_mechanism=False, ): + """Add a mechanism to the construct and all its parts. + + Adds the mechanism to the construct's mechanism dictionary and + propagates it to all parts in the construct. + + Parameters + ---------- + mechanism : Mechanism + The mechanism object to add. + mech_type : str, optional + The mechanism type key. If None, uses the mechanism's + `mechanism_type` attribute. + overwrite : bool, default=False + If True, overwrites existing mechanisms with the same type. If + False, raises ValueError for duplicate types. + optional_mechanism : bool, default=False + If True, suppresses ValueError when a mechanism key conflict + occurs and `overwrite` is False. + + Notes + ----- + This method: + + 1. Adds the mechanism to the construct via the parent Component + class + 2. Propagates the mechanism to each part in the construct + + This ensures mechanism consistency across the entire construct. + + """ Component.add_mechanism( self, mechanism, @@ -246,11 +641,56 @@ def add_mechanism( ) def __repr__(self): - """This is just for display purposes.""" return 'Construct = ' + self.make_name() def __contains__(self, obj2): - """Checks if construct contains a certain part (or copy).""" + """Check if the construct contains a specific part. + + Tests whether a DNA_part or copy of a DNA_part exists in this + construct's parts list. + + Parameters + ---------- + obj2 : DNA_part + The part to search for in the construct. + + Returns + ------- + bool + True if the part (or a copy with the same name and type) is in + the construct, False otherwise. + + Notes + ----- + This method supports two types of containment checks: + + 1. Direct membership: The exact part object is in the construct + (checked via `obj2.parent == self`) + 2. Copy membership: A different part object with the same type and + name exists in the construct + + Examples + -------- + Check if construct contains a part: + + >>> promoter = bcp.Promoter('ptet') + >>> rbs = bcp.RBS('RBS_standard') + >>> cds = bcp.CDS('GFP') + >>> parts = [ + ... [promoter, 'forward'], [rbs, 'forward'], [cds, 'forward'] + ... ] + >>> construct = bcp.Construct( + ... parts_list=parts, + ... name='gene_circuit' + ... ) + >>> promoter in construct + True + + >>> unknown_part = bcp.Promoter('plac') + >>> unknown_part in construct + False + + """ if isinstance(obj2, DNA_part): # if we got a DNA part it could mean one of two things: # 1 we want to know if a dna part is anywhere @@ -288,16 +728,45 @@ def get_species(self): return out_species def located_allcomb(self, spec_list): - """Recursively trace all paths through a list. + """Generate all combinatorial placement dictionaries for species. + + Creates all possible combinations of species placements when multiple + species can bind at different positions in the construct. + + Parameters + ---------- + spec_list : list of Species + List of species that have position attributes, typically + ComplexSpecies that can bind at specific locations. + + Returns + ------- + list of dict + List of dictionaries where each dict maps positions (int) to + [species, direction] pairs representing one possible combinatorial + binding state. + + Notes + ----- + This method handles the combinatorics of placing multiple binding + species at different positions. For example: + + - Species A binds at position 0 + - Species B binds at position 3 + - Species C also binds at position 0 + + The method generates all valid combinations: `{0: [A, direction]}`, + `{0: [C, direction]}`, `{0: [A, direction], 3: [B, direction]}`, + `{0: [C, direction], 3: [B, direction]}`, etc. + + The algorithm: + + 1. Extracts positions from spec_list + 2. Groups species by position into prototype_list + 3. Generates all position combinations via all_comb + 4. For each position combination, generates all possible species + selections at those positions - [[[part1,1],[part2,5]],[[part3,1]],[[part4,5],[part5,12]]] - ====================> - compacted_indexes = [1,5,12] - prototype_list = [[part1,part3],[part2,part4],[part5]] - comb_list = [[1],[5],[12],[1,5],[1,12],[5,12],[1,5,12]] - =========================== - then, take the lists from comb_list and create all possible lists - out of prototype_list that includes those elements """ # first we have to construct the list we are tracing paths through @@ -371,12 +840,39 @@ def recursive_path(in_list): return outdict_list def make_polymers(self, species_lists, backbone): - """Makes polymers from lists of species. + """Create polymer species from combinatorial binding combinations. + + Generates OrderedPolymerSpecies by replacing specific monomers in a + backbone polymer with bound versions according to the replacement + dictionaries. + + Parameters + ---------- + species_lists : list of dict + List of replacement dictionaries where each dict maps positions + (int) to [species, direction] pairs indicating which monomers + to replace with bound versions. + backbone : OrderedPolymerSpecies + The base polymer species serving as the template for creating + bound variants. + + Returns + ------- + list of OrderedPolymerSpecies + List of polymer species representing all specified combinatorial + binding states. + + Notes + ----- + This method takes a backbone polymer (typically the unbound construct) + and creates variants where specific positions contain bound complexes. + + For example, given backbone `` and replacements: + + - `{0: [A:RNAP, forward]}` creates `<[A:RNAP],B,C>` + - `{0: [A:RNAP, forward], 1: [B:RNAP, forward]}` creates + `<[A:RNAP],[B:RNAP],C>` - inputs: - species_lists: list of species which are to be assembled into a - polymer - backbone: the base_species which all these polymers should have """ polymers = [] for combo in species_lists: @@ -394,25 +890,43 @@ def make_polymers(self, species_lists, backbone): return polymers def update_combinatorial_complexes(self, active_components): - """Update complexes formed by combinations of components. + """Generate all combinatorial binding state species for the construct. - Given an input list of components, we produce all complexes yielded - by those components, mixed and matched to make all possible - combinatorial complexes, where each component is assumed to only - care about binding to one spot. + Given components that can bind to different positions in the + construct, this method generates all possible combinations of binding + states by mixing and matching bound species at different locations. - First, the components are asked what species they make, then these - species are sifted to reveal only the ones which are versions of - the same polymer, just with different locations bound. Then, - combinatorial combinations are made. for example: + Parameters + ---------- + active_components : list of Component + Components that generate binding complexes with the construct. + Each is assumed to bind at only one position. - construct: + Returns + ------- + list of OrderedPolymerSpecies + All possible combinatorial binding states of the construct, + including the unbound state and all single and multiple binding + combinations. - two new species are possible: <[A:RNAP],B,C>; + Notes + ----- + The method: - Combinatorial species is also possible (since A and B are assumed - to act independantly) <[A:RNAP],[B:RNAP], C> complexes, where each - component is assumed to only care about binding to one spot. + 1. Collects all binary complex species from each active component + 2. Identifies unique positioned complexes + 3. Generates all combinatorial placements using `located_allcomb` + 4. Creates polymer species for each combination using `make_polymers` + + For example, given construct `` with two components that + create `<[A:RNAP],B,C>` and ``, this method generates: + + - `` (unbound) + - `<[A:RNAP],B,C>` (A bound) + - `` (B bound) + - `<[A:RNAP],[B:RNAP],C>` (both bound) + + assuming A and B act independently. """ species = [] @@ -446,7 +960,37 @@ def update_combinatorial_complexes(self, active_components): # Overwrite Component.enumerate_components def enumerate_constructs(self, previously_enumerated=None): - """Runs all our component enumerators to generate new constructs.""" + """Run all enumerators to generate new construct variants. + + Applies all component enumerators to this construct to generate + derived constructs (e.g., RNA_constructs from transcription). + + Parameters + ---------- + previously_enumerated : set or list, optional + Collection of constructs that have already been enumerated, used + to prevent infinite recursion and duplicate enumeration. + + Returns + ------- + list of Construct + New constructs generated by all enumerators. For DNA_constructs + with the default TxExplorer, this includes all possible + RNA_construct transcripts. + + Notes + ----- + Each enumerator's `enumerate_components` method is called with this + construct and the previously enumerated set, allowing enumerators to + explore transcriptional units, translational products, or other + construct-derived components. + + See Also + -------- + TxExplorer : Default enumerator for DNA transcription exploration. + TlExplorer : Default enumerator for RNA translation exploration. + + """ new_constructs = [] for enumerator in self.component_enumerators: new_comp = enumerator.enumerate_components( @@ -456,26 +1000,37 @@ def enumerate_constructs(self, previously_enumerated=None): return new_constructs def combinatorial_enumeration(self): - """Generate combination of components. + """Generate components for all combinatorial binding states. - Returns a list of new components that are copies of existing - components, but with a different species placed inside. + Creates copies of parts that can react with different combinatorial + binding states of the construct, ensuring reactions are generated for + all possible binding configurations. - This different species represents different combinatorial - states of the polymer. for example: + Returns + ------- + list of Component + Components configured to react with different combinatorial + binding states of the construct. - construct: + Notes + ----- + This method handles the generation of components that account for + multiple simultaneous binding events. For example, given construct + `` where both A and B can bind RNAP: - two new species are possible: <[A:RNAP],B,C>; - combinatorial species is also possible (since A and B are - assumed to act independantly) + - Binary complexes: `<[A:RNAP],B,C>` and `` + - Combinatorial complex: `<[A:RNAP],[B:RNAP],C>` - <[A:RNAP],[B:RNAP],C> + The method returns multiple versions of components A and B, each + configured to bind to different pre-existing binding states: - Thus, this function returns A which binds to (creating - <[A:RNAP],B,C>) AND also A which binds to - (creating <[A:RNAP],[B:RNAP],C>). Likewise for B In total two - A components are returned, and two B components are returned. + - A component binding to `` --> `<[A:RNAP],B,C>` + - A component binding to `` --> `<[A:RNAP],[B:RNAP],C>` + - B component binding to `` --> `` + - B component binding to `<[A:RNAP],B,C>` --> `<[A:RNAP],[B:RNAP],C>` + + This ensures proper reaction enumeration for all binding + combinations. """ # Looks at combinatorial states of constructs to generate DNA_parts @@ -522,29 +1077,48 @@ def combinatorial_enumeration(self): return combinatorial_components def enumerate_components(self, previously_enumerated=None): - """Returns a list of new components and constructs. + """Generate all derived components and constructs from this construct. + + Combines both construct enumeration (e.g., transcripts) and + combinatorial component enumeration (for binding states) to produce + a complete set of derived components. + + Parameters + ---------- + previously_enumerated : set or list, optional + Collection of components already enumerated, used to prevent + infinite recursion. + + Returns + ------- + list of Component + All new components and constructs generated, including: + + - New constructs from enumerators (e.g., RNA_constructs) + - Components for combinatorial binding states - New components are generated if: - - a component creates a species which results in binding to part of - the construct - Example: -> <[A:RNAP],B,C> - Then, A would be returned since a new species is created + Notes + ----- + This method generates new components in two scenarios: - - more than one such component exist in the same construct, for - example: construct: - two new species are possible: <[A:RNAP],B,C>; + 1. Binding-induced species: When a component creates a species + that binds to part of the construct. For example, `` + --> `<[A:RNAP],B,C>` would return component A configured + for this binding. - combinatorial species is also possible (since A and B are assumed - to act independantly) - <[A:RNAP],[B:RNAP], C> + 2. Combinatorial binding states: When multiple components can + bind simultaneously. For construct `` where both A + and B can bind RNAP: - Thus, this function returns A which binds to (creating - <[A:RNAP], B, C>) AND also A which binds to - (creating <[A:RNAP], [B:RNAP], C>). Likewise for B. In total two - A components are returned, and two B components are returned. - New constructs are generated if: self.enumerate_construcs() says - so. For example, in , A is a promoter and makes an - RNA_construct containing + - Binary species: `<[A:RNAP],B,C>` and `` + - Combinatorial: `<[A:RNAP],[B:RNAP],C>` + + Returns components A and B configured to bind to various pre-bound + states, ensuring all binding combinations are enumerated. + + 3. New constructs: Generated by `enumerate_constructs()`. For example, + a DNA_construct with promoter A generates an RNA_construct + containing ``. """ # Runs component enumerator to generate new constructs @@ -559,10 +1133,28 @@ def enumerate_components(self, previously_enumerated=None): @classmethod def get_partstring(cls, part): - """Get string name of a part (including direction). + """Generate a string identifier for a part including its direction. + + Creates a unique string representation of a part that includes its + name and direction but not its position, useful for construct + comparison. + + Parameters + ---------- + part : DNA_part or OrderedMonomer + The part to generate a string identifier for. + + Returns + ------- + str + String combining the part's name and direction (e.g., + 'promoter_pLacforward' or 'gene_GFPreverse'). - A string name of a part including its name and direction (and - not position). + Notes + ----- + This method creates an "orphan" copy of the part (without position + or direction) to get the base name, then appends the direction to + create a direction-aware identifier. """ orphan = part.get_orphan() @@ -574,10 +1166,28 @@ def get_partstring(cls, part): @classmethod def get_partlist_hash(cls, partlist): - """Get hash for a list of parts. + """Generate a hash string for an ordered list of parts. - Creates a string containing the name and direction of all - parts in a list of parts (but not their position). + Creates a unique string identifier for a parts list by concatenating + part names with position indices, capturing the order and content of + parts (but not their absolute positions). + + Parameters + ---------- + partlist : list + List of (part_string, part) tuples where part_string is typically + generated by `get_partstring`. + + Returns + ------- + str + Hash string representing the ordered parts list. + + Notes + ----- + The hash format concatenates each part's string representation with + its index in the list, separated by underscores. This creates a + unique identifier for the parts sequence. """ partlist_str = '_'.join( @@ -590,10 +1200,32 @@ def get_partlist_hash(cls, partlist): @classmethod def create_hashless_reverse(cls, construct): - """Create reverse construction without hash. + """Create a reversed construct without computing its hash. + + Generates a reversed version of the construct with parts in reverse + order and flipped directions, but skips hash computation to avoid + infinite recursion during hash calculations. + + Parameters + ---------- + construct : Construct + The construct to reverse. - Create a reverse construct but don't calculate its hash (because that - would make an infinite loop). + Returns + ------- + Construct + A new construct with reversed parts order and flipped directions, + with `make_dirless_hash=False` to prevent hash computation. + + Notes + ----- + This method is used internally by hash computation routines that need + to compare forward and reverse orientations. Setting + `make_dirless_hash=False` prevents infinite loops where hash + computation would trigger reverse computation, which would trigger + hash computation, etc. + + The circularity status of the construct is preserved. """ rev_con = [a.get_orphan() for a in construct] @@ -609,22 +1241,47 @@ def create_hashless_reverse(cls, construct): @classmethod def rotation_free_hash(cls, construct): - """Compute alphabetically ordered, circular permutation. - - Calculates a unique circular permutation that is the most - alphabetically ordered. - - Every part is considered as a potential starting point, and the most - alphabetically ordered order is then chosen as the best permutation - - Returns: - hash of the most alphabetical ordering, direction of the ordering - (always 1), first position of the best rotation. Thus, to recreate - the conformation of the construct used to make this hash you would - have to - - 1) invert the construct (or not), then - 2) use the indicated position as the first position + """Compute the most alphabetically ordered circular permutation hash. + + Finds the circular permutation of the construct that produces the + most alphabetically ordered sequence of parts, providing a canonical + representation for circular constructs regardless of starting + position. + + Parameters + ---------- + construct : Construct + The circular construct to hash. Should have `circular=True`. + + Returns + ------- + hash : str + String hash of the most alphabetically ordered permutation. + direction : int + Always 1 (forward direction, since only rotation is considered). + first_position : int + The position that should be used as the first position to + recreate this canonical permutation. + + Notes + ----- + This method evaluates every possible starting position for a circular + construct and selects the permutation that produces the most + alphabetically ordered sequence when parts are compared + lexicographically. + + To recreate the canonical form from the original construct: + + 1. Rotate to start at `first_position` + + The direction is always 1 because this method only considers + rotations, not reversals. + + Examples + -------- + For a circular construct with parts A, B, C, if starting at C gives + the most alphabetically ordered sequence (C, A, B), then + `first_position` would be 2. """ @@ -689,18 +1346,43 @@ def circular_next(part, construct): @classmethod def direction_rotation_free_hash(cls, construct): - """Best circular permutation of a construct, forward and reverse. + """Compute the best hash considering both rotation and direction. + + Finds the most alphabetically ordered representation of a circular + construct by evaluating all rotations in both forward and reverse + orientations. + + Parameters + ---------- + construct : Construct + The circular construct to hash. + + Returns + ------- + hash : str + String hash of the most alphabetically ordered permutation in + either direction. + direction : int + Direction of the best ordering: 1 for forward, -1 for reverse. + first_position : int + The position that should be used as the first position in the + best permutation. - Then returns whichever one of those comes alphabetically first. + Notes + ----- + This method: - Returns: - hash of the most alphabetical ordering, direction of the ordering - (1 or -1), first position of the best rotation + 1. Computes the best forward rotation using `rotation_free_hash` + 2. Creates a reversed construct and computes its best rotation + 3. Returns whichever produces the more alphabetically ordered hash - thus, to recreate the conformation of the construct used to make - this hash you would have to - 1) invert the construct (or not), then - 2) use the indicated position as the first position + To recreate the canonical form: + + 1. If direction is -1, reverse the construct + 2. Rotate to start at `first_position` + + This provides complete normalization for circular constructs, + accounting for both rotation and direction symmetries. """ rev_con = Construct.create_hashless_reverse(construct) @@ -713,16 +1395,39 @@ def direction_rotation_free_hash(cls, construct): @classmethod def linear_direction_free_hash(cls, construct): - """String representing the construct forward or reverse. + """Compute the best hash for a linear construct in either direction. + + Determines which orientation (forward or reverse) produces the most + alphabetically ordered sequence for a linear construct. + + Parameters + ---------- + construct : Construct + The linear construct to hash. + + Returns + ------- + hash : str + String hash of the most alphabetically ordered orientation. + direction : int + Direction of the best ordering: 1 for forward, -1 for reverse. + first_position : int + Always 0 for linear constructs (no circular permutation). - Returns: - hash of the most alphabetical ordering, direction of the ordering - (1 or -1), first position of the best rotation (always 0) + Notes + ----- + This method compares forward and reverse orientations part-by-part, + stopping as soon as one orientation is determined to be more + alphabetically ordered than the other. - thus, to recreate the conformation of the construct used to make - this hash you would have to - 1) invert the construct (or not), then - 2) use the indicated position as the first position + To recreate the canonical form: + + 1. If direction is -1, reverse the construct + 2. Start at position 0 (always the first position for linear + constructs) + + Unlike circular constructs, linear constructs have a defined start + and end, so the first_position is always 0. """ rev_con = Construct.create_hashless_reverse(construct) @@ -760,25 +1465,69 @@ def linear_direction_free_hash(cls, construct): @classmethod def omnihash(cls, construct): - """Construct best circullar permutation and ordering. - - A construct can exist forwards or backwards, and circularly permuted - (but only if it's a circular construct). - - This function creates the "best" circular permutation and ordering - of a construct. But the circular permutation is only calculated if - the construct is circular. Best is calculated based on which - orientation/ permutation has the most part names in alphabetical - order. - - Returns: - hash of the most alphabetical ordering (string), direction of the - ordering (1 or -1), first position of the best rotation - - thus, to recreate the conformation of the construct used to make - this hash you would have to - 1) invert the construct (or not), then - 2) use the indicated position as the first position + """Compute a canonical hash for the construct. + + Creates the most alphabetically ordered representation of a construct, + accounting for direction (forward/reverse) and, for circular + constructs, rotation. This provides a unique canonical identifier + for functionally equivalent constructs. + + Parameters + ---------- + construct : Construct + The construct to hash (can be linear or circular). + + Returns + ------- + hash : str + Canonical hash string with 'circular' or 'linear' suffix + indicating construct topology. + direction : int + Direction of the canonical form: 1 for forward, -1 for reverse. + first_position : int + Starting position for the canonical form. For circular constructs, + this is the optimal rotation point. For linear constructs, always + 0. + + Notes + ----- + For circular constructs: + + 1. Evaluates all rotations in both orientations + 2. Selects the most alphabetically ordered permutation + 3. Appends 'circular' to the hash + + For linear constructs: + + 1. Compares forward and reverse orientations + 2. Selects the most alphabetically ordered orientation + 3. Appends 'linear' to the hash + + To recreate the canonical form: + + 1. If direction is -1, reverse the construct + 2. For circular constructs, rotate to start at `first_position` + 3. For linear constructs, `first_position` is always 0 + + This hash enables identification of equivalent constructs that differ + only in representation (rotation or direction). + + Examples + -------- + Two circular constructs with the same parts in different rotations + will produce the same omnihash: + + >>> A, B, C = (bcp.DNA_part(s) for s in ['A', 'B', 'C']) + >>> construct1 = bcp.DNA_construct( + ... [[A, 'forward'], [B, 'forward'], [C, 'forward']], + ... circular=True) + >>> construct2 = bcp.DNA_construct( + ... [[B, 'forward'], [C, 'forward'], [A, 'forward']], + ... circular=True) + >>> hash1, _, _ = bcp.Construct.omnihash(construct1) + >>> hash2, _, _ = bcp.Construct.omnihash(construct2) + >>> hash1 == hash2 + True """ if construct.circular: @@ -796,10 +1545,32 @@ def __hash__(self): return OrderedPolymer.__hash__(self) def __eq__(self, construct2): - """Compare two constructs to see if they are equal. + """Test equality between two constructs. + + Two constructs are considered equal if they have the same string + representation and the same name. + + Parameters + ---------- + construct2 : Construct + The other construct to compare with. + + Returns + ------- + bool + True if constructs are equal, False otherwise. + + Notes + ----- + This is a simple equality test based on string representation. It + does not use deep comparison of parts or the direction-independent + hash. For more sophisticated equivalence testing that accounts for + rotations and reversals, use the `omnihash` method. - Equality means comparing the parts list in a way that is not too - deep. + See Also + -------- + omnihash : Compute canonical hash accounting for rotation and + direction. """ # TODO: make this be a python object comparison @@ -811,23 +1582,205 @@ def __eq__(self, construct2): return False def update_species(self): + """Generate species for the construct. + + Returns + ------- + list of Species + List containing the construct's primary species representation. + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn()` to generate the species associated with this + construct. For most constructs, this returns a single species + representing the entire construct. + + """ species = [self.get_species()] return species def reset_stored_data(self): + """Clear all cached enumeration and prediction data. + + Resets the cached results from component enumeration, RNA prediction, + and protein prediction, forcing these to be recomputed on the next + access. + + Notes + ----- + This method should be called whenever the construct is modified in a + way that would invalidate cached data, such as: + + - Reversing the construct + - Changing parts + - Updating mechanisms or parameters + + The cached attributes that are reset: + + - `out_components`: Results from `enumerate_components` + - `predicted_rnas`: List of predicted RNA products + - `predicted_proteins`: List of predicted protein products + + """ self.out_components = None self.predicted_rnas = None self.predicted_proteins = None def changed(self): + """Handle construct changes by resetting caches and updating name. + + Called when the construct has been modified, this method resets all + cached data and regenerates the construct's name to reflect its + current state. + + Notes + ----- + This method performs two operations: + + 1. Resets all cached enumeration and prediction data via + `reset_stored_data` + 2. Regenerates the construct name via `make_name` to ensure it + reflects the current parts configuration + + This should be called after any structural modification to the + construct. + + """ self.reset_stored_data() self.name = self.make_name() def update_reactions(self, norna=False): + """Generate reactions for the construct. + + Returns + ------- + list of Reaction + Empty list. Base `Construct` class does not generate reactions + directly. Subclasses override this method to provide specific + reaction generation. + + Parameters + ---------- + norna : bool, default=False + If True, RNA-related reactions are excluded (used in some + subclass implementations). + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn()`. The base implementation returns an empty + list because the base Construct class does not generate reactions + directly - reactions are generated by the parts within the construct + through their associated mechanisms. + + Subclasses like `DNA_construct` and `RNA_construct` may override this + to provide construct-specific reaction generation. + + """ return [] class DNA_construct(Construct, DNA): + """DNA construct representing a functional genetic circuit. + + A DNA_construct is a specialized Construct for DNA sequences that can + contain promoters, RBS sites, coding sequences, terminators, and other + genetic elements. It supports transcription to generate RNA constructs and + provides DNA-specific functionality. The class uses the 'transcription' + mechanism to generate RNA products and related species/reactions during + CRN compilation. + + Parameters + ---------- + parts_list : list of list + List of parts in format [[part, direction], ...] where each part + must be a DNA_part or OrderedMonomer. + name : str, optional + Name of the DNA construct. If None, automatically generated. + circular : bool, default=False + If True, represents a circular DNA molecule (e.g., plasmid). + mechanisms : dict or list, optional + Custom mechanisms for this construct, overriding mixture defaults. + parameters : dict, optional + Parameter values specific to this construct. + attributes : list of str, optional + List of attribute tags for the construct. + initial_concentration : float, optional + Initial concentration of the DNA construct. + copy_parts : bool, default=True + If True, makes deep copies of parts when adding to construct. + component_enumerators : list, optional + List of enumerators for generating construct variants. Defaults to + [TxExplorer()] which explores transcriptional variants. + **kwargs + Additional keyword arguments passed to parent constructors. + + Attributes + ---------- + material_type : str + Always 'dna' for DNA constructs. + predicted_rnas : list or None + Cached list of RNA_construct objects that can be transcribed. + predicted_proteins : list or None + Cached list of protein species that can be produced. + + See Also + -------- + Construct : Base class for all constructs. + RNA_construct : RNA version of constructs. + DNA_part : Base class for DNA parts within constructs. + TxExplorer : Default enumerator for transcriptional exploration. + + Notes + ----- + DNA_constructs support several key features: + + - Transcription enumeration: Automatically identifies all possible + transcripts based on promoter positions and orientations + - Protein prediction: Predicts protein products from transcripts + containing RBS sites + - Circular DNA: Special handling for plasmids and other circular + DNA molecules + - Component enumeration: Generates functional variants based on + the genetic parts present + + The default TxExplorer enumerator automatically explores all possible + transcriptional units in the construct. + + Examples + -------- + Create a simple gene expression construct: + + >>> promoter = bcp.Promoter('ptet') + >>> rbs = bcp.RBS('RBS_standard') + >>> cds = bcp.CDS('GFP') + >>> terminator = bcp.Terminator('BBa_B0022') + >>> parts = [ + ... [promoter, 'forward'], [rbs, 'forward'], + ... [cds, 'forward'], [terminator, 'forward'] + ... ] + >>> gene = bcp.DNA_construct( + ... parts_list=parts, + ... name='expression_cassette' + ... ) + + Create a circular plasmid: + + >>> ori = bcp.DNA_part('p15A') + >>> plasmid_parts = [ + ... [ori, 'forward'], [promoter, 'forward'], [rbs, 'forward'], + ... [gene, 'forward'], [terminator, 'forward'] + ... ] + >>> plasmid = bcp.DNA_construct( + ... parts_list=plasmid_parts, + ... name='pUC19_GFP', + ... circular=True, + ... initial_concentration=10 + ... ) + + """ + def __init__( self, parts_list, @@ -866,6 +1819,92 @@ def __repr__(self): class RNA_construct(Construct, RNA): + """RNA construct representing a functional transcript. + + An RNA construct represents an RNA molecule that can be translated into + proteins. Unlike DNA constructs, RNA constructs can only be linear (not + circular) and primarily support translation rather than transcription. + This class uses the 'translation' mechanism to generate protein products + and related species/reactions during CRN compilation. + + Parameters + ---------- + parts_list : list of list + List of parts in format [[part, direction], ...] where parts + represent functional RNA elements (RBS sites, coding sequences, etc.). + name : str, optional + Name of the RNA construct. If None, automatically generated. + promoter : Promoter, optional + Reference to the promoter that produced this RNA transcript. + component_enumerators : list, optional + List of enumerators for generating construct variants. Defaults to + [TlExplorer()] which explores translational variants. + length : int, default=0 + Length of the RNA molecule in nucleotides. + **kwargs + Additional keyword arguments passed to parent constructors. + + Attributes + ---------- + material_type : str + Always 'rna' for RNA constructs. + promoter : Promoter or None + The promoter that controls transcription of this RNA. + length : int + Length of the RNA transcript. + circular : bool + Always False for RNA (RNA cannot be circular). + + See Also + -------- + Construct : Base class for all constructs. + DNA_construct : DNA version of constructs. + TlExplorer : Default enumerator for translational exploration. + RNA : Base class for RNA components. + + Notes + ----- + RNA_constructs have several key characteristics: + + - Linear only: RNA molecules cannot be circular + - Translation focus: Primarily generates protein products through + translation mechanisms + - RBS enumeration: Automatically identifies all ribosome binding + sites and potential translation products + - No transcription: RNA cannot be transcribed to produce other RNA + + The default TlExplorer enumerator automatically explores all possible + translational units in the RNA construct based on RBS positions. + + Examples + -------- + Create an mRNA with RBS and coding sequence: + + >>> rbs1 = bcp.RBS('RBS1') + >>> cds1 = bcp.CDS('GFP') + >>> parts = [[rbs1, 'forward'], [cds1, 'forward']] + >>> mrna = bcp.RNA_construct( + ... parts_list=parts, + ... name='mRNA_GFP' + ... ) + + Create a polycistronic mRNA with multiple RBS-CDS pairs: + + >>> rbs2 = bcp.RBS('RBS2') + >>> cds2 = bcp.CDS('RFP') + >>> strong_promoter = bcp.Promoter('pstrong') + >>> parts = [ + ... [rbs1, 'forward'], [cds1, 'forward'], + ... [rbs2, 'forward'], [cds2, 'forward'] + ... ] + >>> polycistronic = bcp.RNA_construct( + ... parts_list=parts, + ... name='mRNA_operon', + ... promoter=strong_promoter + ... ) + + """ + def __init__( self, parts_list, @@ -875,12 +1914,6 @@ def __init__( length=0, **kwargs, ): - """Linear RNA sequence for translation. - - An RNA_construct is a lot like a DNA_construct except it can - only translate, and can only be linear. - - """ self.material_type = 'rna' self.promoter = promoter self.length = length @@ -900,19 +1933,116 @@ def __init__( RNA.__init__(self=self, name=self.name) def __repr__(self): - """The name of an RNA should be different from DNA, right?""" + # The name of an RNA should be different from DNA, right? return 'RNA_construct = ' + self.name -# # DNA_part: a component-like intermediate class necessary for DNA_construct # Author: Andrey Shur # Latest update: 6/4/2020 # -# - - class DNA_part(Component, OrderedMonomer): + """Base class for individual DNA parts in constructs. + + A DNA_part represents a single functional genetic element (promoter, RBS, + coding sequence, terminator, etc.) that can be assembled into larger + DNA_constructs. Parts have position and direction within constructs and + serve as the modular building blocks for synthetic genetic circuits. + Unlike full Components, DNA_parts do not have initial concentrations - + these must be set on the containing construct or assembly. + + Parameters + ---------- + name : str + Name of the DNA part. + assembly : DNAassembly or OrderedPolymer, optional + The assembly or construct containing this part. + direction : str, optional + Orientation of the part: 'forward' or 'reverse'. + pos : int, optional + Position of this part within its parent construct. + sequence : str, optional + DNA sequence of the part (for future sequence-level modeling). + no_stop_codons : list, optional + List of reading frames without stop codons. Used for identifying + potential coding sequences. Default is empty list. + material_type : str, default='part' + Material classification for the part. + **kwargs + Additional keyword arguments passed to Component constructor. + Note: 'initial_concentration' is not allowed for DNA_parts. + + Attributes + ---------- + name : str + Name of the part. + sequence : str or None + DNA sequence of the part. + material_type : str + Material classification ('part'). + no_stop_codons : list + Reading frames without stop codons. + assembly : DNAassembly or None + Reference to containing assembly. + position : int or None + Position within parent construct. + direction : str + Orientation ('forward' or 'reverse'). + + See Also + -------- + Promoter : DNA_part for transcriptional control. + RBS : DNA_part for translational control. + DNA_construct : Container for multiple DNA_parts. + OrderedMonomer : Base class for positioned elements. + + Raises + ------ + AttributeError + If 'initial_concentration' is provided (not allowed for DNA_parts). + + Notes + ----- + DNA_parts are the fundamental building blocks of genetic constructs: + + - Modular: Can be reused in different constructs + - Directional: Support forward and reverse orientations + - Positional: Track their location within constructs + - No concentration: Cannot have initial concentrations (only + constructs/assemblies can) + + The no_stop_codons attribute is used to identify potential open reading + frames for translation. + + Examples + -------- + Create a generic DNA part: + + >>> part = bcp.DNA_part( + ... name='regulatory_element', + ... direction='forward' + ... ) + + Create a part with sequence information: + + >>> promoter_part = bcp.DNA_part( + ... name='pLac', + ... sequence='ATGCGATCG...', + ... direction='forward' + ... ) + + Use within a construct: + + >>> gene_part = bcp.DNA_part( + ... name='GFP', + ... sequence='TGAGTAAAGGAGAAGAA...', + ... direction='forward' + ... ) + >>> parts = [[promoter_part, 'forward'], [gene_part, 'forward']] + >>> construct = bcp.DNA_construct(parts_list=parts) + + """ + def __init__( self, name, @@ -924,11 +2054,9 @@ def __init__( material_type='part', **kwargs, ): - """Modular component sequence. - - These get compiled into working components - - """ + # Modular component sequence. + # + # These get compiled into working components if 'initial_concentration' in kwargs: raise AttributeError( "DNA_part should not recieve initial_concentration keyword. " @@ -957,6 +2085,12 @@ def __init__( @property def dna_species(self): + """Species: The chemical species representation of this DNA part. + + Returns a Species object with material_type='part' representing this + DNA_part as a chemical species in the CRN. + + """ return Species(self.name, material_type='part') def __repr__(self): @@ -971,6 +2105,36 @@ def __hash__(self): return OrderedMonomer.__hash__(self) + hash(self.name) def __eq__(self, other): + """Test equality between two DNA_parts. + + Parts are equal if they have the same type, name, parent assembly/ + construct, direction, and position. + + Parameters + ---------- + other : DNA_part + The other part to compare with. + + Returns + ------- + bool + True if parts are equal, False otherwise. + + Notes + ----- + Equality requires matching: + + 1. Type (both must be the same DNA_part subclass) + 2. Name (identical names) + 3. Assembly/parent (same parent construct or both have None) + 4. Direction (both forward or both reverse) + 5. Position (same position in parent construct) + + Parts are considered equal even if their parent constructs are + different objects, as long as the string representations of the + parents match. + + """ if type(other) is type(self): if self.name == other.name: if self.assembly is not None and other.assembly is not None: @@ -995,16 +2159,75 @@ def __eq__(self, other): return False def clone(self, position, direction, parent_dna): - """This defines where the part is in what piece of DNA.""" + """Attach this part to a specific position in a DNA construct. + + Parameters + ---------- + position : int + Position in the parent DNA where this part should be placed. + direction : str + Orientation of the part: 'forward' or 'reverse'. + parent_dna : DNA_construct or OrderedPolymer + The DNA construct that will contain this part. + + Returns + ------- + DNA_part + Returns self after setting position and parent. + + Notes + ----- + This method establishes the relationship between a part and its + containing construct, setting the part's position and orientation. + + """ + # Define where the part is in what piece of DNA. # TODO add warning if DNA_part is not cloned self.insert(parent_dna, position, direction) return self def unclone(self): - """Removes the current part from anything.""" + """Remove this part from its parent construct. + + Detaches the part from any parent construct or assembly, resetting + its position and parent references. + + Returns + ------- + DNA_part + Returns self after removal from parent. + + Notes + ----- + This method calls the `remove` method from the `OrderedMonomer` base + class to detach the part from its parent polymer structure. + + After calling this method, the part becomes "orphaned" and can be + attached to a different construct using `clone`. + + See Also + -------- + clone : Attach the part to a construct at a specific position. + + """ self.remove() return self def reverse(self): + """Reverse the orientation of this DNA part. + + Flips the direction of the part between 'forward' and 'reverse'. + + Returns + ------- + DNA_part + Returns self after reversing direction. + + Notes + ----- + This method is typically called when a containing construct is + reversed, ensuring all parts maintain proper relative orientation. + + """ OrderedMonomer.reverse(self) return self diff --git a/biocrnpyler/components/dna/misc.py b/biocrnpyler/components/dna/misc.py index 5367d33c..e53e187f 100644 --- a/biocrnpyler/components/dna/misc.py +++ b/biocrnpyler/components/dna/misc.py @@ -15,10 +15,76 @@ class DNABindingSite(DNA_part): + """DNA binding site component for protein-DNA interactions. + + A DNABindingSite represents a specific DNA sequence where proteins can + bind. This class models protein-DNA binding interactions using the + 'binding' mechanism to generate species and reactions during CRN + compilation. The binding site can accommodate multiple different binding + proteins, each creating separate binding equilibria. + + Parameters + ---------- + name : str + Name of the DNA binding site. + binders : Species, str, or list + Protein species that can bind to this site. Can be a single binder + or a list of multiple binders. + no_stop_codons : bool, optional + If True, indicates the sequence has no stop codons (relevant for + coding sequences). + assembly : DNAassembly, optional + The DNA assembly containing this binding site. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + binders : list of Species + List of protein species that can bind to this site. + dna_to_bind : Species or None + The DNA species that contains this binding site. + mechanisms : dict + Dictionary containing the binding mechanism (defaults to + `One_Step_Cooperative_Binding`). + + See Also + -------- + IntegraseSite : Specialized binding site for integrase proteins. + DNA_part : Base class for DNA component parts. + One_Step_Cooperative_Binding : Default binding mechanism used. + + Notes + ----- + The DNABindingSite uses the 'binding' mechanism to generate binding + reactions for each protein in the `binders` list. Each binder creates + an independent binding equilibrium with the DNA. + + The `dna_to_bind` attribute is set during component enumeration and + represents the actual DNA species containing this binding site. + + Examples + -------- + Create a binding site for a transcription factor: + + >>> binding_site = bcp.DNABindingSite( + ... name='operator_lac', + ... binders='protein_LacI' + ... ) + + Create a binding site with multiple binders: + + >>> binding_site = bcp.DNABindingSite( + ... name='enhancer', + ... binders=['protein_TF1', 'protein_TF2', 'protein_TF3'] + ... ) + + """ + def __init__( - self, name, binders, no_stop_codons=None, assembly=None, **keywords + self, name, binders, no_stop_codons=None, assembly=None, **kwargs ): - """An integrase attachment site binds to integrase.""" + # An integrase attachment site binds to integrase. if isinstance(binders, list): self.binders = [self.set_species(a) for a in binders] else: @@ -30,17 +96,48 @@ def __init__( no_stop_codons=no_stop_codons, mechanisms=self.mechanisms, assembly=assembly, - **keywords, + **kwargs, ) self.name = name self.dna_to_bind = None # self.assembly = None def __repr__(self): + """Return string representation of the binding site. + + Returns + ------- + str + The name of the binding site. + + """ myname = self.name return myname def update_species(self): + """Use 'binding' mechanism to generate protein-DNA species. + + Uses the 'binding' mechanism to generate species for each protein + binder and their DNA-protein complexes when bound to the DNA + containing this binding site. + + Returns + ------- + list of Species + List containing all binder proteins and DNA-protein complexes + generated by the binding mechanism. Returns only the binders + if `dna_to_bind` is None. + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn`. If `dna_to_bind` is not set (None), only + the binder species are returned without generating complexes. + + Each binder generates its own set of binding species with unique + part_id identifiers based on the binder's name. + + """ spec = [] spec += self.binders if self.dna_to_bind is not None: @@ -57,6 +154,25 @@ def update_species(self): return spec def update_reactions(self): + """Use 'binding' mechanism to generate protein-DNA reactions. + + Uses the 'binding' mechanism to generate binding and unbinding + reactions for each protein binder with the DNA containing this binding + site. + + Returns + ------- + list of Reaction + List of binding/unbinding reactions for all binders. Returns + empty list if `dna_to_bind` is None. + + Notes + ----- + This method is called during CRN compilation by + `Mixture.compile_crn`. Each binder generates its own binding + equilibrium with unique kinetic parameters identified by part_id. + + """ rxns = [] if self.dna_to_bind is not None: mech_b = self.mechanisms['binding'] @@ -69,8 +185,32 @@ def update_reactions(self): ) return rxns - def update_component(self, internal_species=None, **keywords): - """Copy of component, except with the proper fields updated.""" + def update_component(self, internal_species=None, **kwargs): + """Create a copy of the binding site with updated DNA reference. + + Used for component enumeration when binding sites are part of larger + DNA constructs that need to be duplicated with different species. + + Parameters + ---------- + internal_species : Species, optional + The new DNA species to bind to. + **kwargs + Additional keyword arguments (currently unused). + + Returns + ------- + DNABindingSite or None + A shallow copy of this binding site with updated `dna_to_bind` + attribute if parent is DNA. Returns None if parent is not DNA. + + Notes + ----- + This method is called during component enumeration to create copies + of binding sites with updated DNA references when DNA constructs + are enumerated into their constituent parts. + + """ if isinstance(self.parent, DNA): out_component = copy.copy(self) out_component.dna_to_bind = internal_species @@ -80,6 +220,102 @@ def update_component(self, internal_species=None, **keywords): class IntegraseSite(DNABindingSite): + """Integrase attachment site for site-specific recombination. + + An IntegraseSite represents a specialized DNA binding site where integrase + proteins can bind and catalyze site-specific recombination. This + component uses the 'binding' mechanism to model integrase-DNA binding and + the 'integration' mechanism to generate recombination reactions. The class + handles both intramolecular (same DNA molecule) and intermolecular + (different DNA molecules) recombination events between compatible + attachment sites (attB/attP producing attL/attR, or similar). + + Parameters + ---------- + name : str + Name of the integrase site. + site_type : str, default='attB' + Type of attachment site. Common types include 'attB', 'attP', + 'attL', 'attR', 'FLP', 'CRE'. Determines recombination compatibility. + integrase : str or Species, default='int1' + The integrase protein that recognizes this site. Can be a string + name or Species object. + dinucleotide : int, default=1 + Specific dinucleotide variant of the attachment site. Different + dinucleotides allow orthogonal recombination systems. + no_stop_codons : bool, optional + If True, indicates the sequence has no stop codons. + integrase_binding : bool, default=True + If True, integrase must bind before recombination. If False, + recombination occurs without explicit binding (simplified model). + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + integrase : Species + The integrase protein species that catalyzes recombination. + dinucleotide : int + The dinucleotide variant identifier. + site_type : str + The type of attachment site (attB, attP, etc.). + other_dna : Species or None + Reference to DNA from another molecule (for intermolecular events). + linked_sites : dict + Dictionary tracking connected recombination partner sites. + complexed_version : Species or None + The integrase-bound version of this site. + integrase_binding : bool + Whether explicit integrase binding is modeled. + + See Also + -------- + DNABindingSite : Parent class for general DNA binding sites. + BasicIntegration : Mechanism for integrase-mediated recombination. + One_Step_Cooperative_Binding : Mechanism for integrase binding. + + Notes + ----- + Integrase sites follow specific recombination rules: + + - attB + attP --> attL + attR (integration) + - attL + attR --> attB + attP (excision) + - Compatible sites must have matching integrases and dinucleotides + + The `linked_sites` dictionary maintains connections between compatible + recombination partners, enabling the generation of appropriate + recombination reactions during CRN compilation. + + Recombination can be intramolecular (creating loops or deletions) or + intermolecular (joining or exchanging DNA segments between molecules). + + Examples + -------- + Create a basic attB site for phage integration: + + >>> attB = bcp.IntegraseSite( + ... name='attB', + ... site_type='attB', + ... integrase='int_phiC31' + ... ) + + Create orthogonal integration sites with different dinucleotides: + + >>> site1 = bcp.IntegraseSite( + ... name='att1', + ... site_type='attB', + ... dinucleotide=1, + ... integrase='int1' + ... ) + >>> site2 = bcp.IntegraseSite( + ... name='att2', + ... site_type='attB', + ... dinucleotide=2, + ... integrase='int1' + ... ) + + """ + def __init__( self, name, @@ -88,7 +324,7 @@ def __init__( dinucleotide=1, no_stop_codons=None, integrase_binding=True, - **keywords, + **kwargs, ): self.update_integrase(integrase) # self.integrase = integrase @@ -103,11 +339,31 @@ def __init__( name, self.integrase, no_stop_codons=no_stop_codons, - **keywords, + **kwargs, ) self.add_mechanism(BasicIntegration(self.integrase.name)) def __repr__(self): + """Return detailed string representation of the integrase site. + + Returns + ------- + str + Formatted string including site name, integrase name, + dinucleotide (if not 1), position, and direction. + + Warns + ----- + UserWarning + If site_type is not in the recognized list of integrase sites. + + Notes + ----- + The representation format is: + 'name_integrase[_dinucleotide][_position][_direction]' + where optional components are included only if set. + + """ myname = self.name if self.site_type in integrase_sites: myname += '_' + self.integrase.name @@ -125,20 +381,108 @@ def __repr__(self): return myname def update_integrase(self, int_name): + """Set or update the integrase protein for this site. + + Parameters + ---------- + int_name : str or Species + Name of the integrase protein or a Species object. + + Notes + ----- + Converts the input to a protein Species object and stores it in + the `integrase` attribute. + + """ self.integrase = Component.set_species( int_name, material_type='protein' ) def __hash__(self): + """Return hash value for the integrase site. + + Returns + ------- + int + Combined hash of the parent DNABindingSite and the dna_to_bind + species. + + Notes + ----- + The hash combines the parent class hash with the dna_to_bind hash + to ensure unique identification of sites bound to specific DNA. + + """ sumhash = DNABindingSite.__hash__(self) + self.dna_to_bind.__hash__() return sumhash def get_complexed_species(self, dna): + """Create the integrase-bound complex for this site. + + Parameters + ---------- + dna : Species + The DNA species containing this integrase site. + + Returns + ------- + Complex + A complex containing the DNA bound by two integrase molecules + (dimeric binding typical of integrases). + + Notes + ----- + Most integrases bind as dimers to catalyze recombination, hence + the complex contains two integrase molecules. + + """ recomp = Complex([dna, self.integrase, self.integrase]) return recomp - def update_component(self, internal_species=None, **keywords): - """Copy of component, except with the proper fields updated.""" + def update_component(self, internal_species=None, **kwargs): + """Create a copy of the integrase site with updated DNA reference. + + This method handles the complex task of copying integrase sites + during component enumeration, maintaining proper linkages between + recombination partner sites. + + Parameters + ---------- + internal_species : Species, optional + The new DNA species containing this integrase site. + **kwargs + Additional keyword arguments. If 'practice_run' is True, performs + special handling to preserve initial site linkage configuration. + + Returns + ------- + IntegraseSite or None + A copy of this integrase site with updated `dna_to_bind` and + properly managed linked site references. Returns None if the + parent DNABindingSite.update_component returns None. + + Notes + ----- + This method manages the complex bookkeeping required for integrase + site recombination: + + Practice Run Mode ('practice_run'=True): During combinatorial + enumeration's practice run, the method preserves the initial + configuration of linked sites by updating references in + partner sites to point to the newly created copy. + + Normal Mode ('practice_run'=False or not specified): + + - For intramolecular reactions: Only populates linked sites if both + recombination partners are bound by integrase (or both unbound if + integrase_binding is False) + - For intermolecular reactions: Always populates linked sites to + enable proper reaction generation + + The method ensures that only one site of a recombination pair + generates the reaction, preventing duplicate reactions in the CRN. + + """ newcomp = DNABindingSite.update_component( self, internal_species=internal_species ) @@ -149,7 +493,7 @@ def update_component(self, internal_species=None, **keywords): # integrase must be bound in order for integrase sites to # do anything return None - elif 'practice_run' in keywords and keywords['practice_run']: + elif 'practice_run' in kwargs and kwargs['practice_run']: # combinatorial enumeration calls update_component twice. an # integrase site must inform all the sites it is linked to that it # has been updated. In certain cases the status of whether a site @@ -222,12 +566,79 @@ def update_component(self, internal_species=None, **keywords): return newcomp def update_species(self): + """Generate species associated with binding and integration. + + Generate the list of species associated with the binding site, + including integrase-DNA complexes generated by the parent DNA binding + site if `integrase_binding` is True. + + Returns + ------- + list of Species + If `integrase_binding` is True, returns species from parent + DNABindingSite including integrase-DNA complexes. If False, + returns only the binder proteins without generating complexes. + + Notes + ----- + When `integrase_binding` is False, the model assumes simplified + recombination without explicit binding steps, useful for simplified + models where binding kinetics are not important. + + """ if self.integrase_binding: return DNABindingSite.update_species(self) else: return self.binders def update_reactions(self): + """Use 'binding' and 'integration' mechanisms to generate reactions. + + Creates binding reactions (if `integrase_binding` is True) and + recombination reactions with linked partner sites. Handles both + intramolecular (same DNA) and intermolecular (different DNA) + recombination events. + + Returns + ------- + list of Reaction + List containing: + + - Integrase binding reactions (if `integrase_binding` is True) + - Recombination reactions with each linked partner site + + Returns empty list if no linked sites exist. + + Notes + ----- + For each linked partner site, the method determines whether to + generate a recombination reaction based on: + + 1. Intramolecular reactions (same DNA molecule): + + - Only generates reaction if both sites are properly bound (or + unbound if integrase_binding is False) + - Prevents duplicate reactions by checking if complex_parent is + already in the linked sites data + - Creates DNA loops, deletions, or inversions + + 2. Intermolecular reactions (different DNA molecules): + + - Generates reactions for all DNA molecules listed in + linked_sites + - These DNAs are added by partner sites that were processed + earlier + - Creates DNA joining, exchange, or integration events + + The method uses the 'integration' mechanism to generate the actual + recombination reactions with appropriate kinetic parameters + identified by the integrase name as part_id. + + Each site pair generates only one reaction (not two) to avoid + duplicates, with the reaction generation responsibility determined + by the update order and population status of linked sites. + + """ if self.integrase_binding: reactions = DNABindingSite.update_reactions(self) else: @@ -325,49 +736,303 @@ def update_reactions(self): class UserDefined(DNA_part): - def __init__(self, name, dpl_type=None, **keywords): - """User-defined part. + """User-defined DNA part with no intrinsic functionality. - A user defined part is a part that doesn't do anything, just exists as - a label basically. + A UserDefined part serves as a placeholder or label in DNA constructs. + It represents a DNA sequence that exists in the construct but does not + use any mechanisms and therefore does not generate any species or + reactions during CRN compilation. This is useful for marking regions of + DNA that are important for visualization, documentation, or future + extension but do not participate in the modeled biochemical processes. - """ - DNA_part.__init__(self, name, **keywords) + Parameters + ---------- + name : str + Name of the user-defined part. + dpl_type : str, optional + Type identifier for DNA Parts Library compatibility. Can be used + to specify the category or function of this part for external + tools or databases. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + name : str + Name of the part. + dpl_type : str or None + DNA Parts Library type identifier. + + See Also + -------- + DNA_part : Base class for DNA component parts. + Origin : Specialized placeholder for origins of replication. + Operator : Specialized placeholder for operator sequences. + + Notes + ----- + UserDefined parts do not use any mechanisms and do not generate any + species or reactions during CRN compilation - both `update_species` + and `update_reactions` return empty lists. + + This component is particularly useful for: + + - Marking spacer sequences or linkers + - Placeholder for parts not yet modeled + - Annotation regions for construct visualization + - Future extension points in genetic designs + + Examples + -------- + Create a spacer sequence: + + >>> spacer = bcp.UserDefined( + ... name='spacer_50bp', + ... dpl_type='spacer' + ... ) + + Create a placeholder for an unmodeled part: + + >>> unknown = bcp.UserDefined( + ... name='unknown_region', + ... dpl_type='uncharacterized' + ... ) + + """ + + def __init__(self, name, dpl_type=None, **kwargs): + # User-defined part. + # + # A user defined part is a part that doesn't do anything, just exists + # as a label basically. + DNA_part.__init__(self, name, **kwargs) self.dpl_type = dpl_type self.name = name def update_species(self): + """Generate species for the user-defined part. + + Returns + ------- + list + Empty list, as user-defined parts have no associated mechanism. + + """ return [] def update_reactions(self): + """Generate reactions for the user-defined part. + + Returns + ------- + list + Empty list, as user-defined parts have no associated mechanism. + + """ return [] class Origin(DNA_part): - def __init__(self, name, **keywords): - """An origin does nothing except look right when plotted.""" - DNA_part.__init__(self, name, **keywords) + """Origin of replication component for visualization. + + An Origin represents an origin of replication (ORI) in a DNA construct. + Like UserDefined parts, it serves primarily as a visual marker and does + not use any mechanisms, therefore it does not generate any species or + reactions during CRN compilation. This component is useful for marking + replication origins in plasmids or other DNA constructs for documentation + and visualization purposes. + + Parameters + ---------- + name : str + Name of the origin of replication. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + name : str + Name of the origin. + + See Also + -------- + DNA_part : Base class for DNA component parts. + UserDefined : General placeholder for non-functional parts. + Operator : Placeholder for operator sequences. + + Notes + ----- + Origins do not use any mechanisms and do not generate any species or + reactions - both `update_species` and `update_reactions` return + empty lists. + + While real origins of replication are essential for plasmid maintenance, + their function is typically not modeled in gene expression CRNs, hence + this component serves as a placeholder for construct annotation. + + Examples + -------- + Create a standard E. coli origin: + + >>> ori = bcp.Origin(name='pUC_ori') + + Create a low-copy origin: + + >>> p15a_ori = bcp.Origin(name='p15A_ori') + + """ + + def __init__(self, name, **kwargs): + # An origin does nothing except look right when plotted. + DNA_part.__init__(self, name, **kwargs) self.name = name def update_species(self): + """Generate species for the origin of replication. + + Returns + ------- + list + Empty list, as origins have no associated mechanism. + + """ return [] def update_reactions(self): + """Generate reactions for the origin of replication. + + Returns + ------- + list + Empty list, as origins have no associated mechanism. + + """ return [] class Operator(DNA_part): - def __init__(self, name, binder=None, **keywords): - """An operator does nothing except look right when plotted.""" - DNA_part.__init__(self, name, **keywords) - self.binder = [] - if binder is not None: - for bind in binder: - self.binder += [Component.set_species(bind)] + """Operator sequence component for visualization. + + An Operator represents an operator DNA sequence (a regulatory element + where repressor proteins typically bind) in a genetic construct. Like + Origin and UserDefined parts, it primarily serves as a visual marker + and does not use any mechanisms, therefore it does not generate species + or reactions during CRN compilation. While the component can store + references to binding proteins, it does not model the actual binding + interactions. + + Parameters + ---------- + name : str + Name of the operator sequence. + binders : Species, str, list, or None, optional + Protein(s) that bind to this operator. Can be a single binder, + a list of binders, or None. This is stored for reference but + does not generate binding reactions. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + name : str + Name of the operator. + binders : list of Species + List of protein species that can bind this operator (for reference + only). + + See Also + -------- + DNA_part : Base class for DNA component parts. + DNABindingSite : Functional binding site that generates reactions. + RegulatedPromoter : Promoter with functional operator-like behavior. + + Notes + ----- + Operators do not use any mechanisms and do not generate any species or + reactions - both `update_species` and `update_reactions` return + empty lists. + + For functional operator behavior with actual binding reactions, use + `DNABindingSite` or include operators within `RegulatedPromoter` + components, which do use binding mechanisms to generate reactions. + + The `binder` attribute stores potential binding proteins for + documentation purposes but does not create binding interactions in + the model. + + Examples + -------- + Create a lac operator: + + >>> lac_op = bcp.Operator( + ... name='lacO', + ... binders='protein_LacI' + ... ) + + Create an operator with multiple potential binders: + + >>> multi_op = bcp.Operator( + ... name='operator_1', + ... binders=['protein_RepA', 'protein_RepB'] + ... ) + + Create an operator without specifying binders: + + >>> generic_op = bcp.Operator(name='op1') + + """ + + def __init__(self, name, binders=None, **kwargs): + # Legacy keyword processing + if kwargs.get('binder', None): + warn("'binder' is deprecated; use 'binders'") + if binders is not None: + raise TypeError("'binder' and 'binders' specified; pick one") + binders = kwargs.pop('binder') + + # An operator does nothing except look right when plotted. + DNA_part.__init__(self, name, **kwargs) + self.binders = [] + if binders is not None: + if not isinstance(binders, list): + binders = [binders] + for bind in binders: + self.binders += [Component.set_species(bind)] self.name = name def update_species(self): + """Generate species for the operator. + + Returns + ------- + list + Empty list, as operators have no associated mechanism. + + Notes + ----- + For functional operator behavior with species generation, use + `DNABindingSite` instead. + + """ return [] def update_reactions(self): + """Generate reactions for the operator. + + Returns + ------- + list + Empty list, as operators have no associated mechanism. + + Notes + ----- + For functional operator behavior with binding reactions, use + `DNABindingSite` or `RegulatedPromoter` instead. + + """ return [] + + @property + def binder(self): + # Legacy attribute + return self.binders diff --git a/biocrnpyler/components/dna/promoter.py b/biocrnpyler/components/dna/promoter.py index 4dad4b47..5393917e 100644 --- a/biocrnpyler/components/dna/promoter.py +++ b/biocrnpyler/components/dna/promoter.py @@ -19,9 +19,81 @@ class Promoter(DNA_part): - """A basic Promoter class with no regulation. - - Needs to be included in a DNAassembly or DNAconstruct to function. + """Basic promoter component for constitutive transcription. + + A promoter represents a DNA regulatory element that controls transcription + of an RNA transcript. This base class implements constitutive + (unregulated) transcription. The component uses the 'transcription' + mechanism to generate species and reactions during CRN compilation. The + promoter must be included in a `DNAassembly` or `DNA_construct` to + function properly. + + Parameters + ---------- + name : str + Name of the promoter. + assembly : DNAassembly, optional + The DNA assembly containing this promoter. If provided, the assembly's + name is used to create the default transcript. + transcript : RNA, str, or None, optional + The RNA transcript produced by this promoter. If None and `assembly` + is provided, creates an RNA species using the assembly's name. Can be + a list of transcripts for multi-cistronic operons. + length : int, default=0 + Length of the promoter sequence in base pairs. + mechanisms : dict or list, optional + Custom mechanisms for this promoter, overriding mixture defaults. + parameters : dict, optional + Parameter values specific to this promoter. + protein : Protein, str, list, or None, optional + Protein product(s) for expression mixtures where transcription is + bypassed. Can be a single protein, list of proteins, or None. + dna_to_bind : DNA or Species, optional + The DNA species that serves as the transcription template. If None, + uses the assembly's DNA when available. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + transcript : Species, list of Species, or None + The RNA transcript(s) produced by transcription. + protein : list of Species or None + Protein product(s) for expression systems. + length : int + Length of the promoter in base pairs. + dna_to_bind : Species or None + The DNA species used as transcription template. + + See Also + -------- + RegulatedPromoter : Promoter with independent regulator binding. + ActivatablePromoter : Promoter with Hill function activation. + RepressiblePromoter : Promoter with Hill function repression. + CombinatorialPromoter : Promoter with combinatorial regulation. + DNAassembly : Container for promoters and genetic constructs. + + Notes + ----- + Promoters cannot have initial concentrations set directly. Initial + conditions must be set on the containing `DNAassembly` or `DNA_construct`. + + Examples + -------- + Create a basic constitutive promoter: + + >>> promoter = bcp.Promoter( + ... name='pconst', + ... transcript='mRNA_gfp' + ... ) + + Create a promoter within an assembly: + + >>> assembly = bcp.DNAassembly(name='gene_x') + >>> promoter = bcp.Promoter( + ... name='p_lac', + ... assembly=assembly + ... ) """ @@ -35,7 +107,7 @@ def __init__( parameters=None, protein=None, dna_to_bind=None, - **keywords, + **kwargs, ): self._dna_bind = dna_to_bind self.length = length @@ -64,21 +136,21 @@ def __init__( # Promoter should not have initial conditions. These need to # be in DNAAssembly or DNAConstruct if ( - 'initial_concentration' in keywords.values() - and keywords['initial_concentration'] is not None + 'initial_concentration' in kwargs.values() + and kwargs['initial_concentration'] is not None ): raise AttributeError( "Cannot set initial_concentration of a Promoter. Must set " "initial_concentration for the DNAassembly or DNAConstruct" ) if ( - 'initial_condition_dictionary' in keywords.values() - and keywords['initial_condition_dictionary'] is not None + 'initial_condition_dictionary' in kwargs.values() + and kwargs['initial_condition_dictionary'] is not None ): raise AttributeError( "Cannot set initial_condition_dictionary of a Promoter. Must " "set initial_condition_dictionary for the DNAassembly or " - "DNAconstruct." + "DNA_construct." ) DNA_part.__init__( @@ -87,10 +159,22 @@ def __init__( mechanisms=mechanisms, parameters=parameters, assembly=assembly, - **keywords, + **kwargs, ) def update_species(self): + """Generate species associated with this promoter. + + Calls the transcription mechanism to generate species for constitutive + transcription from the DNA template. + + Returns + ------- + list of Species + List of species generated by the transcription mechanism, + including RNA polymerase-DNA complexes and transcripts. + + """ mech_tx = self.get_mechanism('transcription') species = [] @@ -106,6 +190,11 @@ def update_species(self): @property def dna_to_bind(self): + """Species or None: DNA species used as transcription template. + + If not explicitly set, defaults to the assembly's DNA species. + + """ if self._dna_bind is None: if self.assembly is None: return None @@ -115,12 +204,42 @@ def dna_to_bind(self): @dna_to_bind.setter def dna_to_bind(self, value): + """Set the DNA species used as transcription template. + + Parameters + ---------- + value : Species or None + The DNA species to use for transcription. + + """ self._dna_bind = value def get_species(self): + """Get the primary species associated with this promoter. + + Returns + ------- + None + Promoters do not have a primary species; they are part of a DNA + assembly. + + """ + # TODO: OK to return None vs empty list? return None def update_reactions(self): + """Generate reactions associated with this promoter. + + Calls the 'transcription' mechanism to generate reactions for + constitutive transcription from the DNA template. + + Returns + ------- + list of Reaction + List of reactions generated by the transcription mechanism, + including RNA polymerase binding and transcript production. + + """ mech_tx = self.get_mechanism('transcription') reactions = [] @@ -134,8 +253,31 @@ def update_reactions(self): ) return reactions - def update_component(self, internal_species=None, **keywords): - """Copy of component, except with the proper fields updated.""" + def update_component(self, internal_species=None, **kwargs): + """Create a copy of the promoter with updated DNA binding target. + + Used for component enumeration when promoters are part of larger + constructs that need to be duplicated with different species. + + Parameters + ---------- + internal_species : Species, optional + The new DNA species to use as the binding target. + **kwargs + Additional keyword arguments (currently unused). + + Returns + ------- + Promoter or None + A shallow copy of this promoter with the updated `dna_to_bind` + attribute. Returns None if parent is RNA. + + Raises + ------ + TypeError + If parent is neither DNA nor RNA construct. + + """ if isinstance(self.parent, RNA): return None elif isinstance(self.parent, DNA): @@ -150,20 +292,55 @@ def update_component(self, internal_species=None, **keywords): # Used for expression mixtures where transcripts are replaced by proteins def get_protein_for_expression(self): - if self.transcript is None: - return self.protein - else: - return None + """Get protein product for expression mixtures. + + In expression mixtures, transcription may be bypassed and translation + may occur directly from DNA. This method returns the protein product + when the gene is expressed. + + Returns + ------- + list of Species or None + The protein product(s) if transcript is None, otherwise None. + + Notes + ----- + This is used by expression mixtures where the transcript species is + omitted and translation occurs directly. + + """ + return self.protein @classmethod def from_promoter(cls, name, assembly, transcript, protein): - """Initialize a promoter instance from another promoter or str. + """Create a promoter instance from another promoter or string. + + Factory method for creating promoters from various input types. + + Parameters + ---------- + name : Promoter or str + Either a string name for a new promoter, or an existing Promoter + object to copy. + assembly : DNAassembly + The assembly containing this promoter. + transcript : RNA or str + The RNA transcript produced by this promoter. + protein : Protein, str, or list + The protein product(s) for expression mixtures. + + Returns + ------- + Promoter + A new Promoter instance. If `name` is a Promoter, returns a + deep copy with updated assembly, transcript, and protein + attributes. + + Raises + ------ + TypeError + If `name` is neither a string nor a Promoter. - :param name: either string or an other promoter instance - :param assembly: - :param transcript: - :param protein: - :return: Promoter instance """ if isinstance(name, Promoter): promoter_instance = copy.deepcopy(name) @@ -186,10 +363,82 @@ def from_promoter(cls, name, assembly, transcript, protein): class RegulatedPromoter(Promoter): - """A Promoter class with simple regulation. + """Promoter with simple independent regulatory binding. + + A regulated promoter allows multiple regulatory proteins (activators or + repressors) to bind independently to the promoter DNA. Each regulator + binds independently, and transcription can occur from both bound and + unbound states with different rates. The component uses the 'binding' + mechanism (`One_Step_Cooperative_Binding` by default) to generate + DNA-regulator complexes and the 'transcription' mechanism to generate + transcription reactions from each regulatory state. + + Parameters + ---------- + name : str + Name of the promoter. + regulators : Species, str, or list + Regulator species that bind to the promoter. Can be a single + regulator or a list. Each regulator binds independently. + leak : bool, default=True + If True, allows transcription from the unbound promoter state (leak + expression). If False, only bound states transcribe. + assembly : DNAassembly, optional + The assembly containing this promoter. + transcript : RNA or str, optional + The RNA transcript produced by this promoter. + length : int, default=0 + Length of the promoter in base pairs. + mechanisms : dict or list, optional + Custom mechanisms for this promoter. + parameters : dict, optional + Parameter values specific to this promoter. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + regulators : list of Species + List of protein species that regulate this promoter. + leak : bool + Whether leak transcription (from unbound state) is allowed. + complexes : list of Species + List of DNA-regulator complexes generated during compilation. + + See Also + -------- + Promoter : Base promoter class. + ActivatablePromoter : Hill function-based activation. + RepressiblePromoter : Hill function-based repression. + CombinatorialPromoter : Combinatorial regulation logic. + + Notes + ----- + Each regulator binds independently, creating multiple DNA-protein + complexes. Transcription can occur from each complex with different + parameters identified by part_id. + + The leak behavior allows modeling of constitutive expression that occurs + even without regulator binding. + + Examples + -------- + Create a promoter with a single regulator: + + >>> promoter = bcp.RegulatedPromoter( + ... name='p_reg', + ... regulators='protein_TF', + ... leak=True + ... ) + + Create a promoter with multiple independent regulators: + + >>> promoter = bcp.RegulatedPromoter( + ... name='p_multi', + ... regulators=['protein_TF1', 'protein_TF2'], + ... leak=False + ... ) - regulators = [list of species] - Each regulator binds independently to the Promoter to regulate it. """ def __init__( @@ -202,7 +451,7 @@ def __init__( length=0, mechanisms=None, parameters=None, - **keywords, + **kwargs, ): Promoter.__init__( self, @@ -212,7 +461,7 @@ def __init__( length=length, mechanisms=mechanisms, parameters=parameters, - **keywords, + **kwargs, ) if not isinstance(regulators, list): @@ -230,6 +479,31 @@ def __init__( self.complexes = [] def update_species(self): + """Generate species for regulated transcription. + + Uses the 'transcription' and 'binding' mechanisms to generate species + for regulator-DNA binding and transcription from each regulatory + state. Generates DNA-regulator complexes and identifies which + complexes can transcribe. + + Returns + ------- + list of Species + List containing all DNA-regulator complexes and species generated + by the transcription mechanism for each regulatory state. + + Notes + ----- + The method generates: + + - DNA-regulator binding complexes for each regulator + - Transcription-related species for unbound DNA (if leak is True) + - Transcription-related species for each DNA-regulator complex + + Complexes are stored in `self.complexes` for use in + `update_reactions`. + + """ mech_tx = self.get_mechanism('transcription') mech_b = self.get_mechanism('binding') species = [] @@ -276,6 +550,27 @@ def update_species(self): return species def update_reactions(self): + """Generate reactions for regulated transcription. + + Uses the 'binding' mechanism to generate binding reactions for each + regulator and the 'transcription' mechanism to generate transcription + reactions for each regulatory state (bound and unbound). + + Returns + ------- + list of Reaction + List containing all binding reactions for regulators and + transcription reactions for each DNA state. + + Notes + ----- + Reactions are generated for: + + - Regulator binding and unbinding to DNA + - Transcription from unbound DNA (if leak is True) + - Transcription from each DNA-regulator complex + + """ reactions = [] mech_tx = self.get_mechanism('transcription') mech_b = self.get_mechanism('binding') @@ -313,13 +608,70 @@ def update_reactions(self): class ActivatablePromoter(Promoter): - """Promoter activated by single species, modeled as Hill function.""" + r"""Promoter with Hill function-based activation. + + An activatable promoter models transcriptional activation by a single + regulator species using Hill function kinetics. The component uses a + 'transcription' mechanism (`PositiveHillTranscription`) to generate + species and reactions where the transcription rate increases with + activator concentration following cooperative binding dynamics. + + Parameters + ---------- + name : str + Name of the promoter. + activator : Species or str + The activator protein species that enhances transcription. + transcript : RNA or str, optional + The RNA transcript produced by this promoter. + leak : bool, default=False + If True, allows basal transcription without activator. If False, no + transcription occurs without activator binding. + **kwargs + Additional keyword arguments passed to parent `Promoter` class. + + Attributes + ---------- + activator : Species + The activator protein that enhances transcription. + leak : bool + Whether basal (leak) transcription is allowed. + + See Also + -------- + RepressiblePromoter : Hill function-based repression. + RegulatedPromoter : Independent regulator binding. + PositiveHillTranscription : Mechanism for Hill activation. + + Notes + ----- + The activation follows a Hill function: + + .. math:: + + \text{rate} = k_{\text{max}} \frac{[A]^n}{K_d^n + [A]^n} + + k_{\text{leak}} + + where [A] is activator concentration, n is the Hill coefficient, and + :math:`K_d` is the dissociation constant. + + Examples + -------- + Create an activatable promoter: + + >>> promoter = bcp.ActivatablePromoter( + ... name='p_ara', + ... activator='protein_AraC', + ... leak=True + ... ) + + """ def __init__( - self, name, activator, transcript=None, leak=False, **keywords + self, name, activator, transcript=None, leak=False, **kwargs ): - # Always call the superclass __init__() with **keywords passed through - Promoter.__init__(self, name=name, transcript=transcript, **keywords) + # Always call the superclass __init__() with **kwargs passed through + Promoter.__init__(self, name=name, transcript=transcript, **kwargs) # Set the Regulator # Component.set_species( @@ -336,7 +688,22 @@ def __init__( PositiveHillTranscription(), 'transcription', overwrite=True ) - def update_species(self, **keywords): + def update_species(self, **kwargs): + """Use 'transcription' mechanism to generate activation species. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the transcription + mechanism. + + Returns + ------- + list of Species + List of species generated by the transcription mechanism for + Hill-based activation. + + """ # Mechanisms are stored in an automatically created # dictionary: mechanism_type --> Mechanism Instance. mech_tx = self.get_mechanism('transcription') @@ -354,7 +721,21 @@ def update_species(self, **keywords): return species - def update_reactions(self, **keywords): + def update_reactions(self, **kwargs): + """Use 'transcription' mechanism to generate activation reactions. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the transcription + mechanism. + + Returns + ------- + list of Reaction + List of reactions for Hill-based transcriptional activation. + + """ mech_tx = self.get_mechanism('transcription') reactions = [] # a list of reactions must be returned @@ -373,13 +754,70 @@ def update_reactions(self, **keywords): class RepressiblePromoter(Promoter): - """Promoter repressed by single species, modeled Hill function.""" + r"""Promoter with Hill function-based repression. + + A repressible promoter models transcriptional repression by a single + regulator species using Hill function kinetics. The component uses a + 'transcription' mechanism (`NegativeHillTranscription`) to generate + species and reactions where the transcription rate decreases with + repressor concentration following cooperative binding dynamics. + + Parameters + ---------- + name : str + Name of the promoter. + repressor : Species or str + The repressor protein species that inhibits transcription. + transcript : RNA or str, optional + The RNA transcript produced by this promoter. + leak : bool, default=False + If True, allows residual transcription even at high repressor + concentrations. If False, transcription is fully repressed. + **kwargs + Additional keyword arguments passed to parent `Promoter` class. + + Attributes + ---------- + repressor : Species + The repressor protein that inhibits transcription. + leak : bool + Whether leak transcription at high repressor is allowed. + + See Also + -------- + ActivatablePromoter : Hill function-based activation. + RegulatedPromoter : Independent regulator binding. + NegativeHillTranscription : Mechanism for Hill repression. + + Notes + ----- + The repression follows a Hill function: + + .. math:: + + \text{rate} = k_{\text{max}} \frac{K_d^n}{K_d^n + [R]^n} + + k_{\text{leak}} + + where [R] is repressor concentration, n is the Hill coefficient, and + :math:`K_d` is the dissociation constant. + + Examples + -------- + Create a repressible promoter: + + >>> promoter = bcp.RepressiblePromoter( + ... name='p_lac', + ... repressor='protein_LacI', + ... leak=False + ... ) + + """ def __init__( - self, name, repressor, transcript=None, leak=False, **keywords + self, name, repressor, transcript=None, leak=False, **kwargs ): - # Always call the superclass __init__() with **keywords passed through - Promoter.__init__(self, name=name, transcript=transcript, **keywords) + # Always call the superclass __init__() with **kwargs passed through + Promoter.__init__(self, name=name, transcript=transcript, **kwargs) # Set the Regulator # Component.set_species( @@ -396,7 +834,22 @@ def __init__( NegativeHillTranscription(), 'transcription', overwrite=True ) - def update_species(self, **keywords): + def update_species(self, **kwargs): + """Use 'transcription' mechanism to generate repression species. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the transcription + mechanism. + + Returns + ------- + list of Species + List of species generated by the 'transcription' mechanism for + Hill-based repression. + + """ # Mechanisms are stored in an automatically created # dictionary: mechanism_type --> Mechanism Instance. mech_tx = self.get_mechanism('transcription') @@ -410,12 +863,26 @@ def update_species(self, **keywords): part_id=self.name + '_' + self.repressor.name, leak=self.leak, protein=self.get_protein_for_expression(), - **keywords, + **kwargs, ) return species - def update_reactions(self, **keywords): + def update_reactions(self, **kwargs): + """Use 'transcription' mechanism to generate repression reactions. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the 'transcription' + mechanism. + + Returns + ------- + list of Reaction + List of reactions for Hill-based transcriptional repression. + + """ mech_tx = self.get_mechanism('transcription') reactions = [] # a list of reactions must be returned @@ -428,12 +895,118 @@ def update_reactions(self, **keywords): part_id=self.name + '_' + self.repressor.name, leak=self.leak, protein=self.get_protein_for_expression(), - **keywords, + **kwargs, ) return reactions class CombinatorialPromoter(Promoter): + """Promoter with combinatorial regulatory logic. + + A combinatorial promoter allows multiple regulators to bind cooperatively, + where transcription behavior depends on the specific combination of bound + regulators. The component uses the 'binding' mechanism + (`Combinatorial_Cooperative_Binding`) to generate all possible + DNA-regulator complexes and the 'transcription' mechanism to generate + reactions for each regulatory state. This enables complex logic gates + (AND, OR, NOR, etc.) and multi-input regulatory functions. + + Parameters + ---------- + name : str + Name of the promoter. + regulators : Species, str, or list + List of regulator species that can bind to the promoter. Regulators + can bind in various combinations. + leak : bool, default=False + If True, allows transcription from promoter states not in + `tx_capable_list` (including unbound state). If False, only states in + `tx_capable_list` transcribe. + assembly : DNAassembly, optional + The assembly containing this promoter. + transcript : RNA or str, optional + The RNA transcript produced by this promoter. + length : int, default=0 + Length of the promoter in base pairs. + mechanisms : dict or list, optional + Custom mechanisms for this promoter. + parameters : dict, optional + Parameter values specific to this promoter. + protein : Protein, str, list, or None, optional + Protein product(s) for expression mixtures. + tx_capable_list : list of list, optional + List specifying which combinations of bound regulators enable + transcription. Each element is a list of regulator names (strings or + Species). If None, all combinations enable transcription. + cooperativity : dict, optional + Dictionary mapping regulator names to their cooperativity values + (Hill coefficients) for binding, e.g., {'regulator1': 2, + 'regulator2': 1}. + **kwargs + Additional keyword arguments passed to parent class. + + Attributes + ---------- + regulators : list of Species + Sorted list of protein regulators (sorted for consistency). + cooperativity : dict or None + Cooperativity values for each regulator. + tx_capable_list : list of set + List of regulator combinations (as sets) that enable transcription. + leak : bool + Whether leak transcription is allowed for non-capable states. + complex_combinations : dict + Dictionary mapping combinations to their DNA-regulator complexes. + tx_capable_complexes : list of Species + List of DNA complexes that can transcribe. + leak_complexes : list of Species + List of DNA complexes that transcribe with leak parameters. + + See Also + -------- + RegulatedPromoter : Independent regulatory binding. + Combinatorial_Cooperative_Binding : Binding mechanism used by this + promoter. + + Notes + ----- + Only combinations in `tx_capable_list` transcribe with full rate; + others transcribe with leak rate (if leak is True) or not at all. + + Regulators are automatically sorted alphabetically to ensure consistent + ordering when checking combinations. + + Examples + -------- + Create an AND gate promoter (transcribes only when both bound): + + >>> promoter = bcp.CombinatorialPromoter( + ... name='p_and', + ... regulators=['TF1', 'TF2'], + ... tx_capable_list=[['TF1', 'TF2']], + ... leak=False + ... ) + + Create an OR gate promoter (transcribes when either is bound): + + >>> promoter = bcp.CombinatorialPromoter( + ... name='p_or', + ... regulators=['TF1', 'TF2'], + ... tx_capable_list=[['TF1'], ['TF2'], ['TF1', 'TF2']], + ... leak=False + ... ) + + Create a promoter with cooperative binding: + + >>> promoter = bcp.CombinatorialPromoter( + ... name='p_coop', + ... regulators=['TF1', 'TF2'], + ... cooperativity={'TF1': 2, 'TF2': 1}, + ... tx_capable_list=[['TF1', 'TF2']] + ... ) + + """ + def __init__( self, name, @@ -447,46 +1020,8 @@ def __init__( protein=None, tx_capable_list=None, cooperativity=None, - **keywords, + **kwargs, ): - """Combinatorial promoter. - - Binding multiple regulators results in qualitatively different - transcription behavior. For example, maybe it's an AND gate - promoter where it only transcribes if two regulators are - bound, but not if either one is bound. - - ============= - inputs - ============= - name: the name of the promoter - regulators: a list of strings or species indicating all - the possible regualtors that can bind - - leak: if true, then a promoter with nothing bound will transcribe - - assembly: a DNA_assembly object that contains this promoter - - transcript: the transcript that this promoter makes - - length: the length in nt? I don't think this is used for anything at - the moment - - mechanisms: additional mechanisms. formatted with - {"mechanism_type":mechanismObject(),...} - - parameters: promoter-specific parameters. Formatted as - {("identifier1","identifier2"):value,...} - - tx_capable_list: list of which combination of regulators bound will - lead to transcription. Formatted as - [["regulator1","regulator2"],["regulator1"],...] regulators can be - strings or Species - - cooperativity: a dictionary of cooperativity values. For example, - {"regulator":2,"regulator2":1,....} - - """ Promoter.__init__( self, name=name, @@ -496,7 +1031,7 @@ def __init__( mechanisms=mechanisms, parameters=parameters, protein=protein, - **keywords, + **kwargs, ) if not isinstance(regulators, list): @@ -548,6 +1083,38 @@ def __init__( ) def update_species(self): + """Generate species for combinatorial regulatory logic. + + Uses the 'transcription' and 'binding' mechanisms to generate all + possible DNA-regulator complexes through cooperative binding and + identifies which complexes enable transcription based on + `tx_capable_list`. + + Returns + ------- + list of Species + List containing: + + - The unbound DNA + - All regulator species + - All DNA-regulator binding complexes + - Transcription-related species for capable complexes + - Transcription-related species for leak complexes (if leak is + True) + + Notes + ----- + The method classifies DNA-regulator complexes into two categories: + + - `tx_capable_complexes`: Complexes that match combinations in + `tx_capable_list` and transcribe with full rate parameters + - `leak_complexes`: Complexes not in `tx_capable_list` that + transcribe with leak parameters (if leak is True) + + Complexes are stored in these attributes for use by + `update_reactions`. + + """ mech_tx = self.get_mechanism('transcription') mech_b = self.get_mechanism('binding') # set the tx_capable_complexes to nothing because we havent updated @@ -620,6 +1187,38 @@ def update_species(self): return species def update_reactions(self): + """Generate reactions for combinatorial regulatory logic. + + Uses the 'transcription' and 'binding' mechanisms to generate binding + reactions for all regulator combinations and transcription reactions + for capable and leak complexes. + + Returns + ------- + list of Reaction + List containing: + + - Cooperative binding reactions for all regulator combinations + - Transcription reactions from unbound DNA (if leak is True) + - Transcription reactions from capable complexes + - Transcription reactions from leak complexes (if leak is True) + + Warns + ----- + UserWarning + If no complexes can transcribe after calling `update_species`. + + Notes + ----- + This method automatically calls `update_species` if the complex + lists are empty, ensuring that species generation occurs before + reaction generation. + + Each transcription reaction uses a unique part_id that identifies the + regulatory state, constructed from the promoter name and bound + regulators (e.g., 'p_name_TF1_TF2_RNAP'). + + """ reactions = [] mech_tx = self.get_mechanism('transcription') mech_b = self.get_mechanism('binding') diff --git a/biocrnpyler/components/dna/rbs.py b/biocrnpyler/components/dna/rbs.py index 90eb5c7c..b507d835 100644 --- a/biocrnpyler/components/dna/rbs.py +++ b/biocrnpyler/components/dna/rbs.py @@ -9,9 +9,79 @@ class RBS(DNA_part): - """A simple RBS class with no regulation. + """Ribosome binding site component for translation control. - Must be included in a DNAconstruct or DNAassembly to do anything. + An RBS (ribosome binding site) represents a regulatory element that + controls translation of a protein from an RNA transcript. The component + uses the 'translation' mechanism to generate species and reactions for + ribosome binding and protein production. The RBS must be included in a + `DNAassembly` or `DNA_construct` to function properly during CRN + compilation. + + Parameters + ---------- + name : str + Name of the RBS. + assembly : DNAassembly, optional + The DNA assembly containing this RBS. If provided, the assembly's + name is used to generate default transcript and protein species. + transcript : RNA, str, or None, optional + The RNA transcript containing this RBS. If None and `assembly` is + provided, creates an RNA species using the assembly's name. + protein : Protein, str, or None, optional + The protein product of translation. If None and `assembly` is + provided, creates a Protein species using the assembly's name. + length : int, default=0 + Length of the RBS sequence in base pairs. + mechanisms : dict or list, optional + Custom mechanisms for this RBS, overriding mixture defaults. + parameters : dict, optional + Parameter values specific to this RBS. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + transcript : Species or None + The RNA transcript containing the RBS. + protein : Species or None + The protein product of translation. + assembly : DNAassembly or None + The DNA assembly containing this RBS. + length : int + Length of the RBS in base pairs. + + See Also + -------- + Promoter : Component for transcription control. + DNAassembly : Container for RBS and genetic constructs. + DNA_part : Base class for DNA component parts. + + Notes + ----- + The RBS cannot have initial concentrations set directly. Initial + conditions must be set on the containing `DNAassembly` or `DNA_construct`. + + The translation mechanism generates reactions for ribosome binding to + the transcript and subsequent protein production. + + Examples + -------- + Create a basic RBS: + + >>> rbs = bcp.RBS( + ... name='rbs1', + ... transcript='mRNA_gfp', + ... protein='protein_gfp' + ... ) + + Create an RBS within an assembly: + + >>> assembly = bcp.DNAassembly(name='gene_x') + >>> rbs = bcp.RBS( + ... name='rbs_strong', + ... assembly=assembly + ... ) """ @@ -24,17 +94,16 @@ def __init__( length=0, mechanisms=None, parameters=None, - **keywords, + **kwargs, ): - self.assembly = assembly self.length = length - DNA_part.__init__( self, name=name, mechanisms=mechanisms, parameters=parameters, - **keywords, + assembly=assembly, + **kwargs, ) if transcript is None and assembly is None: @@ -54,6 +123,18 @@ def __init__( self.protein = self.set_species(protein, material_type='protein') def update_species(self): + """Use the 'translation' mechanism to generate translation species. + + Uses the 'translation' mechanism to generate species for ribosome + binding and protein production from the RNA transcript. + + Returns + ------- + list of Species + List of species generated by the translation mechanism, + including ribosome-RNA complexes and protein products. + + """ mech_tl = self.get_mechanism('translation') species = [] species += mech_tl.update_species( @@ -65,6 +146,18 @@ def update_species(self): return species def update_reactions(self): + """Use the 'translation' mechanism to generate translation reactions. + + Uses the 'translation' mechanism to generate reactions for ribosome + binding to the transcript and protein production. + + Returns + ------- + list of Reaction + List of translation reactions including ribosome binding and + protein synthesis. Returns empty list if protein is None. + + """ mech_tl = self.get_mechanism('translation') reactions = [] @@ -77,8 +170,32 @@ def update_reactions(self): ) return reactions - def update_component(self, internal_species=None, **keywords): - """Copy of component, except with the proper fields updated.""" + def update_component(self, internal_species=None, **kwargs): + """Create a copy of the RBS with updated transcript reference. + + Used for component enumeration when RBS is part of larger constructs + that need to be duplicated with different species. + + Parameters + ---------- + internal_species : Species, optional + The new transcript species to use for this RBS copy. + **kwargs + Additional keyword arguments (currently unused). + + Returns + ------- + RBS or None + A shallow copy of this RBS with the updated `transcript` + attribute if parent is RNA and direction is 'forward'. Returns + None otherwise. + + Raises + ------ + AttributeError + If direction attribute has an unknown value. + + """ if isinstance(self.parent, DNA): return None elif isinstance(self.parent, RNA): @@ -99,13 +216,33 @@ def update_component(self, internal_species=None, **keywords): @classmethod def from_rbs(cls, name, assembly, transcript, protein): - """Initialize an RBS instance from another RBS or str. + """Create an RBS instance from another RBS or string. + + Factory method for creating RBS objects from various input types. + + Parameters + ---------- + name : RBS or str + Either a string name for a new RBS, or an existing RBS object + to copy. + assembly : DNAassembly + The assembly containing this RBS. + transcript : RNA or str + The RNA transcript containing the RBS. + protein : Protein or str + The protein product of translation. + + Returns + ------- + RBS + A new RBS instance. If `name` is an RBS, returns a deep copy + with updated assembly, transcript, and protein attributes. + + Raises + ------ + TypeError + If `name` is neither a string nor an RBS. - :param name: either string or an other rbs instance - :param assembly: - :param transcript: - :param protein: - :return: RBS instance """ if isinstance(name, RBS): rbs_instance = copy.deepcopy(name) diff --git a/biocrnpyler/components/dna/terminator.py b/biocrnpyler/components/dna/terminator.py index dc35e334..2b4efacb 100644 --- a/biocrnpyler/components/dna/terminator.py +++ b/biocrnpyler/components/dna/terminator.py @@ -5,12 +5,96 @@ class Terminator(DNA_part): - def __init__(self, name, **keywords): - DNA_part.__init__(self, name, **keywords) + """Transcriptional terminator component for ending transcription. + + A Terminator represents a DNA sequence that signals the end of + transcription, causing RNA polymerase to dissociate from the DNA + template and release the newly synthesized RNA transcript. This + component serves as a structural annotation within genetic constructs + but does not directly generate species or reactions during CRN + compilation. + + Parameters + ---------- + name : str + Name of the terminator. + **kwargs + Additional keyword arguments passed to the parent `DNA_part` class. + + Attributes + ---------- + name : str + Name of the terminator. + + See Also + -------- + Promoter : Component that initiates transcription. + DNAassembly : Container for terminators and other genetic parts. + DNA_part : Base class for DNA component parts. + + Notes + ----- + The Terminator component itself does not generate any species or + reactions during CRN compilation. It serves primarily as a structural + element to mark the end of transcription units in genetic constructs. + + Termination behavior, if modeled, would typically be implemented through + termination efficiency parameters in the transcription mechanism rather + than through the terminator component itself. + + Examples + -------- + Create a basic terminator: + + >>> terminator = bcp.Terminator(name='T7_terminator') + + Use a terminator in a DNA construct: + + >>> promoter = bcp.Promoter('ptet') + >>> rbs = bcp.RBS('RBS_standard') + >>> cds = bcp.CDS('GFP') + >>> terminator = bcp.Terminator('BBa_B0022') + >>> construct = bcp.DNA_construct( + ... [promoter, rbs, cds, terminator], + ... name='complete_gene' + ... ) + + Create a terminator with custom attributes: + + >>> terminator = bcp.Terminator( + ... name='BBa_B0015', + ... attributes=['double_terminator'] + ... ) + + """ + + def __init__(self, name, **kwargs): + DNA_part.__init__(self, name, **kwargs) self.name = name def update_species(self): + """Generate species associated with this terminator. + + Returns + ------- + list of Species + Empty list as terminator components have no associated mechanism. + + """ return [] def update_reactions(self): + """Generate reactions associated with this terminator. + + Returns + ------- + list of Reaction + Empty list as terminator components have no associated mechanism. + + Notes + ----- + Termination behavior is typically modeled through + transcription mechanism parameters. + + """ return [] diff --git a/biocrnpyler/components/integrase_enumerator.py b/biocrnpyler/components/integrase_enumerator.py index 0cb14d3d..2b47c6a8 100644 --- a/biocrnpyler/components/integrase_enumerator.py +++ b/biocrnpyler/components/integrase_enumerator.py @@ -12,43 +12,104 @@ from .dna.misc import IntegraseSite -class Polymer_transformation: # TODO: rename using standard conventions +class Polymer_transformation: + """Template for transforming polymer sequences through recombination. + + A `Polymer_transformation` defines a template for creating new polymers + from existing ones through recombination reactions. The template + specifies a parts list containing placeholders (monomers from input + polymers) and new parts (with no parent). This enables complex DNA + rearrangements like integration, deletion, and inversion. + + Parameters + ---------- + partslist : list + List of parts defining the output polymer. Can contain: + + - OrderedMonomers from existing polymers (placeholders) + - Parts with parent=None (inserted into new polymer) + - Tuples of (part, direction) + + circular : bool, default=False + Whether the output polymer should be circular. + parentsdict : dict, optional + Dictionary mapping parent polymers to input names ('input1', + 'input2', etc.). If None, automatically generated. + material_type : str, default='dna' + Material type for the created polymer. + + Attributes + ---------- + number_of_inputs : int + Number of distinct input polymers required. + parentsdict : dict + Mapping from parent polymers to generic input names. + partslist : list + Template parts list with dummy placeholders. + circular : bool + Whether output is circular. + material_type : str + Material type of output polymer. + + See Also + -------- + IntegraseRule : Defines integrase recombination rules. + Integrase_Enumerator : Enumerates integrase products. + OrderedMonomer : Monomer in ordered polymer. + + Notes + ----- + The transformation works by: + + 1. Analyzing partslist to identify parent polymers + 2. Creating generic 'input#' placeholders for each parent + 3. Storing template with dummy placeholders + 4. When applied via `create_polymer`, replacing placeholders with + actual parts from input polymers + + Placeholder system: + + - Parts from polymers become placeholders referencing position in + 'input#' + - Parts with parent=None are copied directly into output + - Complexes bound to parts are transferred to new positions + + Examples + -------- + Create a simple transformation template: + + >>> # Define template: take element 0 from input1, element 2 from + >>> # input2 (reversed), and insert a promoter + >>> template = Polymer_transformation( + ... partslist=[ + ... polymer1[0], # Placeholder for position 0 + ... [polymer2[2], 'reverse'], # Position 2, reversed + ... promoter # New part (parent=None) + ... ], + ... circular=False + ... ) + >>> # Apply template to create new polymer + >>> new_polymer = template.create_polymer([polymer1, polymer2]) + + Integration reaction template: + + >>> # Combine two plasmids at cut sites + >>> template = Polymer_transformation( + ... partslist=( + ... plasmid1[:cut1] + + ... [[prod_site1, 'forward']] + + ... plasmid2[cut2+1:] + + ... [[prod_site2, 'forward']] + + ... plasmid1[cut1+1:] + ... ), + ... circular=True + ... ) + + """ + def __init__( self, partslist, circular=False, parentsdict=None, material_type='dna' ): - """Initalized a polymer transformation. - - A Polymer transformation is like a generic transformation of a polymer - sequence. You specify a parts list that would make up the output - polymer. This list can contain: - - parts from ordered polymers - parts that aren't in any polymers (have parent = None) - parts from ordered polymers are considered as "placeholders" - parts with parent = None are inserted into the new polymer. - - Also you can specify if the output should be circular or not. - - Example: - valid partslist: - partslist = [ - Monomer(forward,3,"input1"), - Monomer(reverse,1,"input2"), - Promoter(forward,None,None) - ] - - then, self.create_polymer([polymer1,polymer2]) takes element 3 from - polymer1 and puts it forward, element 1 from polymer 2, and a Promoter - object and creates a new polymer by feeding these three monomers into - a polymer constructor. - - new_polymer.parts_list = [ - polymer1[3].setdir("forward"), - polymer2[1].setdir("reverse"), - [Promoter,"forward"] - ] - - """ if parentsdict is None: # the input to this function is a list of monomers that belong to # various parents. Each different parent that is represented in @@ -131,10 +192,22 @@ def __init__( self.material_type = material_type def renumber_output(self, output_renumbering_function): - """Change the ordering of the output list. + """Change the ordering of the output parts list. + + Applies a renumbering function to rearrange the parts in the + template, useful for handling circular permutations or reversals. + + Parameters + ---------- + output_renumbering_function : callable + Function that takes an index (int) and returns tuple of + (new_index, direction), where direction is 'f' (forward) or + 'r' (reverse). - Use the output_renumbering_function, which takes in an int and - returns an int which is the new index of the part. + Notes + ----- + Modifies self.partslist in place. Used to adjust transformations + when matching against existing constructs. """ new_partslist = [] @@ -150,17 +223,40 @@ def renumber_output(self, output_renumbering_function): self.partslist = new_partslist def get_renumbered(self, output_renumbering_function): - """Return copy of transformation with output indexes renumbered.""" + """Return copy of transformation with output indexes renumbered. + + Parameters + ---------- + output_renumbering_function : callable + Function mapping old indexes to (new_index, direction) tuples. + + Returns + ------- + Polymer_transformation + Copy of this transformation with renumbered output. + + """ rxn_copied = copy.copy(self) rxn_copied.renumber_output(output_renumbering_function) return rxn_copied def reversed(self): - """Return a circularly permuted version of self. + """Return circularly permuted version with rotated inputs. + + Creates a new transformation where inputs are shuffled: input1 + becomes input2, input2 becomes input3, ..., last input becomes + input1. Used for handling symmetric integrase reactions. - That means the inputs are shuffled around For example, we had input1, - input2, input3. Now we will have input1=input2, input2=input3, - input3=input1. + Returns + ------- + Polymer_transformation + New transformation with rotated input assignments. + + Notes + ----- + For single-input transformations, returns self unchanged. Essential + for bidirectional integrase reactions where site1/site2 roles can + be swapped. """ new_parentsdict = {} @@ -192,13 +288,42 @@ def reversed(self): ) def create_polymer(self, polymer_list, **kwargs): - """Create a new polymer from the template saved inside this class. + """Create a new polymer from the template using input polymers. + + Applies the transformation template to concrete input polymers, + replacing placeholders with actual parts and inserting new parts. + + Parameters + ---------- + polymer_list : list of Polymer + List of input polymers. Must have at least number_of_inputs + polymers. Order matters: first polymer is 'input1', second is + 'input2', etc. + **kwargs + Additional keyword arguments (currently unused). + + Returns + ------- + Polymer + New polymer created by applying the transformation. Type + matches the input polymers' class. + + Notes + ----- + Transformation process: - A polymer_list is a list of polymers from which the resulting polymer - is made. Some of the parts which compose the output polymer don't have - a parent, and therefore are new parts. In these cases anything bound - to the previous location of these parts will be bound to the new ones - as well. + 1. Map polymer_list to 'input#' names + 2. For each template part: + + - If placeholder: grab part from appropriate input polymer at + specified position + - If new part: insert the part or its dna_species + - Handle complex species bound to parts + + 3. Create output polymer with specified circularity + + When transforming parts with bound complexes, the method attempts + to preserve bindings by replacing core parts within complexes. """ polymer_dict = { @@ -293,11 +418,30 @@ def create_polymer(self, polymer_list, **kwargs): @classmethod def dummify(cls, in_polymer, name): - """Create a simplified, disconnected polymer. - - The polymer that has the same number of monomers, direction of - monomers, and name as the input polymer, but is otherwise - disconnected. + """Create a simplified placeholder polymer. + + Generates a generic polymer with the same structure (length, + directions, circularity) as the input but without specific parts. + Used for creating template placeholders. + + Parameters + ---------- + in_polymer : Polymer + The polymer to simplify. + name : str + Name for the dummy polymer (e.g., 'input1'). + + Returns + ------- + NamedPolymer + Simplified polymer with generic OrderedMonomers at each + position. + + Notes + ----- + The dummy polymer preserves only structural information (number of + monomers, their directions, and circularity) while removing + specific part identities. """ # this is used specifically with polymerTransformation. Dummified @@ -312,6 +456,15 @@ def dummify(cls, in_polymer, name): return NamedPolymer(out_list, name, circular=circular) def __repr__(self): + """Return string representation of the transformation. + + Returns + ------- + str + Human-readable string showing the transformation template with + part names, positions, and directions. + + """ part_texts = [] for plist in self.partslist: part = plist[0] @@ -339,6 +492,122 @@ def __repr__(self): class IntegraseRule: + """Rules defining integrase recombination reactions and products. + + An `IntegraseRule` specifies how an integrase enzyme acts on + attachment sites to generate recombined DNA products. Defines which + site pairs can react, what products they form, and which reaction + types (deletion, integration, inversion) are allowed. + + Parameters + ---------- + name : str, optional + Name of the integrase (default='int1'). + reactions : dict, optional + Dictionary mapping (site1_type, site2_type) tuples to product + site type. Default: {('attB', 'attP'): 'attL', + ('attP', 'attB'): 'attR'} + allow_deletion : bool, default=True + Whether to allow deletion reactions (intramolecular, same + direction). + allow_integration : bool, default=True + Whether to allow integration reactions (intermolecular). + allow_inversion : bool, default=True + Whether to allow inversion reactions (intramolecular, opposite + directions). + + Attributes + ---------- + name : str + Integrase name. + integrase_species : Species + The integrase protein species. + reactions : dict + Reaction rules mapping site pairs to products. + attsites : list + All attachment site types involved in reactions. + allow_deletion : bool + Whether deletions are allowed. + allow_integration : bool + Whether integrations are allowed. + allow_inversion : bool + Whether inversions are allowed. + integrations_to_do : list + List of integrations to perform during compilation. + + See Also + -------- + IntegraseSite : DNA part representing attachment sites. + Integrase_Enumerator : Enumerator using integrase rules. + Polymer_transformation : Template for DNA transformations. + + Notes + ----- + Integrase mechanism types: + + 1. Serine Integrases: + + - Recombine attB + attP --> attL + attR + - Require matching dinucleotides + - With directionality factors: attL + attR --> attB + attP + + 2. Tyrosine Recombinases (Cre, Flp): + + - Homotypic sites: loxP + loxP --> loxP + loxP + - Can be palindromic (bidirectional) + + 3. Invertases: + + - Only perform inversion reactions + - Set allow_deletion=False, allow_integration=False + + 4. Resolvases: + + - Only perform deletion reactions + - Set allow_inversion=False, allow_integration=False + + Reaction Types: + + - Inversion: Two sites on same DNA, opposite directions --> + region between sites flips + - Deletion: Two sites on same DNA, same direction --> region + between sites excised (forms circular product) + - Integration: Sites on different DNAs --> DNAs join + - Recombination: Two linear DNAs --> two recombinant linear DNAs + + Examples + -------- + Define a standard serine integrase: + + >>> int_rule = bcp.IntegraseRule( + ... name='PhiC31', + ... reactions={ + ... ('attB', 'attP'): 'attL', + ... ('attP', 'attB'): 'attR' + ... } + ... ) + + Define a Cre recombinase (homotypic): + + >>> cre_rule = bcp.IntegraseRule( + ... name='Cre', + ... reactions={ + ... ('loxP', 'loxP'): 'loxP', + ... ('loxP', 'loxP'): 'loxP' # Symmetric + ... } + ... ) + + Define an invertase (inversion only): + + >>> inv_rule = bcp.IntegraseRule( + ... name='Hin', + ... reactions={('hixL', 'hixR'): 'hixL', ('hixR', 'hixL'): 'hixR'}, + ... allow_deletion=False, + ... allow_integration=False + ... ) + + """ + def __init__( self, name=None, @@ -347,14 +616,6 @@ def __init__( allow_integration=True, allow_inversion=True, ): - """The integrase mechanism is a mechanism at the level of DNA. - - It creates DNA species which the integrase manipulations would lead - to. This mechanism does not create any reaction rates. We need to - figure out how integrase binding will work before being able to create - reactions and their corresponding rates - - """ if reactions is None: reactions = {('attB', 'attP'): 'attL', ('attP', 'attB'): 'attR'} if name is None: @@ -374,9 +635,38 @@ def __init__( self.integrations_to_do = [] def binds_to(self): + """Get all attachment site types this integrase binds to. + + Returns + ------- + list of str + List of all site types involved in integrase reactions. + + """ return self.attsites def reaction_allowed(self, site1, site2): + """Check if two sites can undergo integrase recombination. + + Parameters + ---------- + site1 : IntegraseSite + First attachment site. + site2 : IntegraseSite + Second attachment site. + + Returns + ------- + bool + True if sites can react according to reaction rules. + + Raises + ------ + AssertionError + If sites have different integrases or do not match this + integrase. + + """ assert isinstance(site1, IntegraseSite) assert isinstance(site2, IntegraseSite) assert site1.integrase == site2.integrase @@ -386,7 +676,14 @@ def reaction_allowed(self, site1, site2): return False def reactive_sites(self): - """Attachment sites that participate in integrase reactions.""" + """Get attachment site types that participate in reactions. + + Returns + ------- + list of str + List of site types that can be reactants (not just products). + + """ attsites = [] for reaction in self.reactions: attsites += list(reaction) @@ -394,7 +691,41 @@ def reactive_sites(self): return attsites def generate_products(self, site1, site2, site2_parent=None): - """DNA_part objects corresponding to the products of recombination.""" + """Generate product sites from recombination of two sites. + + Creates IntegraseSite objects for the products of site1 + site2 + recombination according to the reaction rules. + + Parameters + ---------- + site1 : IntegraseSite + First attachment site (determines product ordering). + site2 : IntegraseSite + Second attachment site. + site2_parent : Polymer, optional + Parent polymer for site2 (used in intermolecular reactions). + + Returns + ------- + tuple of (IntegraseSite, IntegraseSite) + Product sites at positions corresponding to site1 and site2. + + Raises + ------ + AssertionError + If sites have mismatched integrases or dinucleotides. + KeyError + If site pair is not in reaction rules. + + Notes + ----- + Product sites inherit dinucleotides and integrase from reactants. + Product order depends on site1 direction: + + - site1 forward: return (prod1, prod2) + - site1 reverse: return (prod2, prod1) with swapped directions + + """ # the sites should have the same integrase and dinucleotide, otherwise # it won't work assert site1.integrase == site2.integrase @@ -471,39 +802,93 @@ def integrate( force_inter=False, existing_dna_constructs=None, ): - """Perform an integration reaction between the chosen sites. - - Make new DNA_constructs site1 and site2 are integrase site dna_parts - which have parents that are DNA_constructs. - - There are four possible reactions: - 1) inversion - two sites are part of the same dna construct - the result is another dna construct with the same circularity and - the region in between the sites flipped - 2) deletion - two sites are part of the same dna construct - the result is two dna constructs: one with the same circularity - but the region between the sites deleted, and another - circular dna construct that contains the deleted portion - 3) integration - the sites are on two different dna constructs - the result is a single dna construct - 4) recombination - the sites are on two different dna constructs - the results are two different dna constructs with the proper - portions swapped after the correct dna constructs are generated, - the reactions which were done to produce them are encoded into - polymer_transformations and "baked into" the integrase sites - themselves. So, each integrase site knows which specific integrase - reactions it should produce when it comes time to update_reactions. - - also_inter controls whether intramolecular reactions should also - generate intermolecular reactions that occur between two copies of the - same plasmid. - - force_inter forces a reaction to be intermolecular even if the two - sites are on the same plasmid. + """Perform integrase recombination between two attachment sites. + + Executes an integration reaction between site1 and site2, creating + new DNA constructs based on the reaction type (inversion, deletion, + integration, or recombination). Stores transformation templates in + the sites' linked_sites attribute. + + Parameters + ---------- + site1 : IntegraseSite + First attachment site (must have Construct parent). + site2 : IntegraseSite + Second attachment site (must have Construct parent). + also_inter : bool, default=True + If True and reaction is intramolecular, also generate + intermolecular version (between two copies of same plasmid). + force_inter : bool, default=False + Force reaction to be treated as intermolecular even if sites + are on same construct. + existing_dna_constructs : list of Construct, optional + List of previously generated constructs to check for + duplicates. + + Returns + ------- + list of Construct + List of newly created DNA constructs from the integration. + + Raises + ------ + ValueError + If either site is not part of a Construct. + + Notes + ----- + Four reaction types: + + 1. Inversion (intramolecular, opposite directions): + + - Same construct, sites point opposite directions + - Result: Region between sites is flipped + - Circularity preserved + + 2. Deletion (intramolecular, same direction): + + - Same construct, sites point same direction + - Result: Two constructs - one with deleted region, one + circular excised fragment + + 3. Integration (intermolecular, one circular): + + - Sites on different constructs, one circular + - Result: Single construct (circular if both were circular) + + 4. Recombination (intermolecular, both linear): + + - Sites on two linear constructs + - Result: Two recombinant linear constructs + + Polymer_transformation templates are stored in: + + - site1.linked_sites[(site2, intermolecular)] + - site2.linked_sites[(site1, intermolecular)] + + These templates are used during CRN compilation to generate + reactions and species. + + Existing_dna_constructs are checked for matches (including circular + permutations and reversals) to avoid creating duplicates. + + Examples + -------- + Inversion reaction: + + >>> # Two sites on same plasmid, opposite directions + >>> int_rule.integrate(attB_site, attP_site_reversed) + # Creates inverted plasmid + + Integration reaction: + + >>> # Sites on different plasmids + >>> int_rule.integrate( + ... plasmid1_attB, + ... plasmid2_attP, + ... existing_dna_constructs=prev_constructs + ... ) + # Creates integrated plasmid """ intermolecular = True # by default, the reaction is intermolecular @@ -751,6 +1136,97 @@ def integrate( class Integrase_Enumerator(GlobalComponentEnumerator): + """Global enumerator for integrase-mediated DNA recombination products. + + An `Integrase_Enumerator` systematically enumerates all possible DNA + constructs that can result from integrase-mediated recombination + reactions. Examines all components for integrase attachment sites and + generates products for all allowed site pairs. + + Parameters + ---------- + name : str + Name identifier for the enumerator. + int_mechanisms : dict, optional + Dictionary mapping integrase names (str) to IntegraseRule objects. + Default: {'int1': IntegraseRule()} + + Attributes + ---------- + int_mechanisms : dict + Dictionary of integrase rules. + + See Also + -------- + GlobalComponentEnumerator : Base class for global enumeration. + IntegraseRule : Defines integrase recombination rules. + IntegraseSite : DNA part for attachment sites. + Polymer_transformation : Template for DNA rearrangements. + + Notes + ----- + Enumeration process: + + 1. Identify all integrase attachment sites in components + 2. Group sites by integrase type + 3. For each integrase: + + a. Find all valid site pairs (from reactive_sites) + b. Check if pair can react (reaction_allowed) + c. Perform integration to generate products + d. Store transformation templates in sites + + 4. Return list of new DNA constructs + + This is a global enumerator because integrase reactions can occur + between sites on different constructs (intermolecular reactions). + Access to all components is necessary. + + Integrase Types Supported: + + - Serine integrases (attB/attP --> attL/attR) + - Tyrosine recombinases (Cre, Flp with homotypic sites) + - Invertases (inversion only) + - Resolvases (deletion only) + - Custom integrase rules + + The `find_dna_construct` method is used to detect duplicates including + circular permutations and reversals, preventing redundant construct + generation. + + Examples + -------- + Create an integrase enumerator: + + >>> phi_c31 = bcp.IntegraseRule( + ... name='PhiC31', + ... reactions={ + ... ('attB', 'attP'): 'attL', + ... ('attP', 'attB'): 'attR' + ... } + ... ) + >>> enumerator = bcp.Integrase_Enumerator( + ... name='integrase_enum', + ... int_mechanisms={'PhiC31': phi_c31} + ... ) + + Use in a mixture: + + >>> mixture = bcp.Mixture( + ... components=[plasmid1, plasmid2], + ... global_component_enumerators=[enumerator] + ... ) + >>> # Enumerator automatically called during compilation + >>> crn = mixture.compile_crn() + + Manual enumeration: + + >>> constructs = [plasmid_with_attB, plasmid_with_attP] + >>> new_constructs = enumerator.enumerate_components(constructs) + >>> # new_constructs contains integrated plasmids + + """ + def __init__(self, name: str, int_mechanisms=None): if int_mechanisms is None: int_mechanisms = {'int1': IntegraseRule()} @@ -758,7 +1234,20 @@ def __init__(self, name: str, int_mechanisms=None): GlobalComponentEnumerator.__init__(self, name=name) def list_integrase(self, construct): - """Lists all the parts that can be acted on by integrases.""" + """List all integrase attachment sites in a construct. + + Parameters + ---------- + construct : Construct + DNA construct to examine. + + Returns + ------- + dict + Dictionary mapping integrase names (str) to lists of + IntegraseSite objects. + + """ int_dict = {} for part in construct.parts_list: if isinstance(part, IntegraseSite) and part.integrase is not None: @@ -772,7 +1261,24 @@ def list_integrase(self, construct): return int_dict def reset(self, components=None, **kwargs): - """This resets the linked_sites member in any attachment sites.""" + """Reset linked_sites attribute in all attachment sites. + + Clears stored integration reactions from all integrase sites in + components, preparing for fresh enumeration. + + Parameters + ---------- + components : list of Component + Components containing integrase sites to reset. + **kwargs + Additional keyword arguments (unused). + + Notes + ----- + Called at the start of enumeration to clear previous integration + data. + + """ for component in components: if hasattr(component, 'parts_list'): for part in component: @@ -783,13 +1289,46 @@ def reset(self, components=None, **kwargs): def find_dna_construct( cls, construct: Construct, conlist: List[Construct] ): - """Find a construct that matches the input 'construct'. + """Find matching construct in list (handles permutations/reversals). + + Searches for a construct equivalent to the input, accounting for + circular permutations and reversals. + + Parameters + ---------- + construct : Construct + Construct to find. + conlist : list of Construct + List of constructs to search. + + Returns + ------- + tuple of (Construct, callable) or None + If found: (matched_construct, index_function), where + index_function maps old indexes to (new_index, direction). + If not found: None. + + Raises + ------ + KeyError + If construct matches multiple constructs in list (should not + happen with proper generation order). + + Notes + ----- + For circular constructs, the following matching logic is used: + + - Try all circular permutations + - For each permutation, try forward and reverse orientations + + For linear constructs, the following matching logic is used: + + - Try forward orientation + - Try reverse orientation - Can be reverse or circularly permuted. + Uses `directionless_hash` for fast initial filtering before detailed + species comparison. - returns: found_construct, index_function(index) => - (new_index,"f" or "r" if it must be reversed) - or, None, if no matching construct is found """ matched_construct = None for other_construct in conlist: @@ -851,23 +1390,71 @@ def find_dna_construct( def enumerate_components( self, components=None, previously_enumerated=None, **kwargs ): - """Explort all the possible integrase-motivated DNA configurations. - - If some integrases aren't present, then define intnames to be a list - of names of the integrases which are present. - - An integrase can act in different ways: - * serine integrases recombine B and P sites that turn into - L and R sites, and only sites with the same dinucleotide can - be recombined. - * serine integrases with directionality factors recombine L and R - sites with the same dinucleotide - * Invertases only do flipping reactions - * resolvases only do deletion reactions - * FLP or CRE react with homotypic sites, so site1+site1 = site1+site1. - But there are still different types of sites which are orthogonal. - For example, a CRE type 1 or a CRE type 2 site. The sites can also - be palindromic, which means that they can react in either direction. + """Enumerate all possible integrase-mediated DNA configurations. + + Systematically generates all DNA constructs that can result from + integrase recombination between attachment sites in the input + components. + + Parameters + ---------- + components : list of Component, optional + List of components to enumerate. Only DNA_construct objects + are processed. + previously_enumerated : list of Component, optional + List of components already enumerated (used for duplicate + detection). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Construct + List of newly created DNA constructs from all allowed + integrase reactions. + + Notes + ----- + Enumeration algorithm: + + 1. Extract all DNA_construct components + 2. List all integrase sites by integrase type + 3. For each integrase in int_mechanisms: + + a. Get all attachment sites for that integrase + b. Find reactive site types from IntegraseRule + c. Generate all site pairs (combinations) + d. For each valid pair: + + - Check if reaction_allowed + - Perform `integrate` to generate products + - Add new constructs to output list + + 4. Return all newly generated constructs + + Depending on the `IntegraseRule` settings, the following reaction + types are generated: + + - Inversions (same construct, opposite directions) + - Deletions (same construct, same direction) + - Integrations (different constructs, at least one circular) + - Recombinations (two linear constructs) + + The `integrate` method checks existing_dna_constructs (includes + both previously_enumerated and newly created constructs) to avoid + generating duplicates. + + Examples + -------- + Enumerate integration products: + + >>> enumerator = bcp.Integrase_Enumerator( + ... name='enum', + ... int_mechanisms={'PhiC31': phi_c31_rule} + ... ) + >>> plasmids = [donor_plasmid, target_plasmid] + >>> products = enumerator.enumerate_components(plasmids) + >>> # products contains integrated plasmids """ if previously_enumerated is None: diff --git a/biocrnpyler/components/membrane.py b/biocrnpyler/components/membrane.py index 8fefc4b5..000d1a00 100644 --- a/biocrnpyler/components/membrane.py +++ b/biocrnpyler/components/membrane.py @@ -10,11 +10,73 @@ class DiffusibleMolecule(Component): - """A class to represent passive diffusion. - - This class is to classify a molecule that will diffuse passively - through the membrane. By default, a DiffusibleMolecule uses a - mechanism called 'diffusion'. + """Molecule that diffuses passively through a membrane. + + A `DiffusibleMolecule` component represents a molecule that undergoes + passive diffusion across a membrane between two compartments. The + component uses a 'diffusion' mechanism to generate bidirectional + diffusion reactions based on concentration gradients. + + Parameters + ---------- + substrate : Species, str, or Component + The diffusible molecule species. Can be a `Species` object, string + name, or `Component` with an associated species. + internal_compartment : str or Compartment, default='Internal' + The internal compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + external_compartment : str or Compartment, default='External' + The external compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + attributes : list of str, optional + List of attribute tags to associate with the substrate species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + substrate : Species + The substrate species in the internal compartment. + product : Species + The same substrate species in the external compartment (diffusion + product). + + See Also + -------- + MembraneChannel : Active transport through membrane channels. + MembranePump : ATP-dependent active transport. + Component : Base class for biomolecular components. + + Notes + ----- + Passive diffusion follows concentration gradients and does not require + energy. The diffusion mechanism generates bidirectional reactions: + + - Forward: substrate_internal --> substrate_external + - Reverse: substrate_external --> substrate_internal + + If not specified using the `name` keyword, the component name is + automatically generated as: '_' + + Examples + -------- + Create a simple diffusible molecule: + + >>> glucose = bcp.DiffusibleMolecule( + ... substrate='Glucose', + ... internal_compartment='Cytoplasm', + ... external_compartment='Extracellular' + ... ) + + Use with a mixture and diffusion mechanism: + + >>> mixture = bcp.Mixture( + ... components=[glucose], + ... mechanisms={'diffusion': bcp.Simple_Diffusion()}, + ... parameters={'k_diff': 0.01} + ... ) + >>> crn = mixture.compile_crn() """ @@ -24,17 +86,8 @@ def __init__( internal_compartment: Union[str, Compartment] = 'Internal', external_compartment: Union[str, Compartment] = 'External', attributes=None, - **keywords, + **kwargs, ): - """Initialize a DiffusibleMolecule object. - - :param substrate: name of the diffusible substrate, reference to - an Species or Component - :param internal_compartment: name of internal compartment - :param external_compartment: name of external compartment - :param attributes: Species attribute, passed to Component - :param keywords: pass into the parent's (Component) initializer - """ # Creates compartment object if compartment is a str if isinstance(internal_compartment, str): internal_compartment = Compartment(name=internal_compartment) @@ -50,20 +103,53 @@ def __init__( ) # Name the component - name = self.substrate.name + '_' + self.substrate.compartment.name + if (name := kwargs.pop('name', None)) is None: + name = self.substrate.name + '_' + self.substrate.compartment.name Component.__init__( - self=self, name=name, attributes=attributes, **keywords + self=self, name=name, attributes=attributes, **kwargs ) def get_species(self): + """Get the substrate species in the internal compartment. + + Returns + ------- + Species + The substrate species in the internal compartment. + + """ return self.substrate def update_species(self): + """Use 'diffusion' mechanism to generate diffusion species. + + Uses the 'diffusion' mechanism to generate species in both + compartments. + + Returns + ------- + list of Species + List of species in internal and external compartments generated + by the diffusion mechanism. + + """ mech_diff = self.get_mechanism('diffusion') return mech_diff.update_species(self.substrate, self.product) def update_reactions(self): + """Use 'diffusion' mechanism to generate diffusion reactions. + + Uses the 'diffusion' mechanism to generate reactions for passive + diffusion between compartments. + + Returns + ------- + list of Reaction + List of diffusion reactions (forward and reverse) between + internal and external compartments. + + """ mech_diff = self.get_mechanism('diffusion') return mech_diff.update_reactions( self.substrate, self.product, component=self, part_id=self.name @@ -71,12 +157,86 @@ def update_reactions(self): class IntegralMembraneProtein(Component): - """Transmembrane proteins or integral membrane proteins. - - This membrane class is to classify a membrane channel that will intergrate - into the membrane. Uses a mechanism called "membrane_insertion". Size is - used to indicate number of repeating components to create oligomer. Dimer - = 2, Trimers = 3, etc. + """Transmembrane protein that integrates into the membrane. + + An `IntegralMembraneProtein` component represents a membrane protein + that integrates into a membrane compartment. The component uses a + 'membrane_insertion' mechanism to generate reactions for protein + insertion into the membrane. The size parameter allows modeling of + oligomeric channels (dimers, trimers, etc.). + + Parameters + ---------- + membrane_protein : Species, str, or Component + The membrane protein species before insertion. Can be a `Species` + object, string name, or `Component` with an associated species. + product : Species, str, or Component + The integrated membrane protein species. Can be a `Species` object, + string name, or `Component`. + direction : str, optional + Transport direction attribute for the integrated protein. + Default is 'Passive'. Common values: 'Passive', 'Importer', + 'Exporter'. + size : int, optional + Number of monomers needed to form the functional channel. Used to + model oligomeric channels (e.g., size=2 for dimers, size=3 for + trimers). Default is 1. + compartment : str or Compartment, default='Internal' + The compartment containing the membrane protein before insertion. + Can be a string name or `Compartment` object. + membrane_compartment : str or Compartment, default='Membrane' + The membrane compartment where the protein integrates. Can be a + string name or `Compartment` object. + attributes : list of str, optional + List of attribute tags to associate with the membrane protein. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + membrane_protein : Species + The membrane protein species before insertion. + product : Species + The integrated transmembrane protein species in the membrane + compartment. + + See Also + -------- + MembraneChannel : Membrane channel for substrate transport. + Component : Base class for biomolecular components. + + Notes + ----- + The membrane_insertion mechanism generates reactions for protein + integration into the membrane. For oligomeric channels, the size + parameter determines the stoichiometry: + + - size=1: Monomer insertion + - size=2: Dimer formation (2 proteins --> 1 channel) + - size=3: Trimer formation (3 proteins --> 1 channel) + + The component name is automatically generated as: + '_' + + Examples + -------- + Create a simple membrane protein: + + >>> channel = bcp.IntegralMembraneProtein( + ... membrane_protein='ChannelProtein', + ... product='ChannelProtein_membrane', + ... direction='Passive' + ... ) + + Create a dimeric channel protein: + + >>> dimer = bcp.IntegralMembraneProtein( + ... membrane_protein='Aquaporin', + ... product='Aquaporin_channel', + ... size=2, + ... direction='Passive' + ... ) """ @@ -89,22 +249,8 @@ def __init__( compartment: Union[str, Compartment] = 'Internal', membrane_compartment: Union[str, Compartment] = 'Membrane', attributes=None, - **keywords, + **kwargs, ): - """Initialize a IntegralMembraneProtein object. - - :param product: name of the membrane channel, reference to an - Species or Component - :param direction: transport direction (str), set to "Passive" by - default, undirectional unless specified - :param size: number of monomers needed for channel used in - Membrane_Protein_Integration(Mechanism) - :param internal_compartment: name of internal compartment - :param membrane_compartment: name of membrane compartment - :param attributes: Species attribute. - :param keywords: pass into the parent's (Component) initializer - - """ # Creates compartment object if compartment is a str if isinstance(compartment, str): compartment = Compartment(name=compartment) @@ -187,16 +333,48 @@ def __init__( + self.membrane_protein.compartment.name ) - Component.__init__(self=self, name=name, **keywords) + Component.__init__(self=self, name=name, **kwargs) def get_species(self): + """Get the membrane protein species before insertion. + + Returns + ------- + Species + The membrane protein species in the compartment before + integration into the membrane. + + """ return self.membrane_protein def update_species(self): + """Use 'membrane_insertion' to generate membrane insertion species. + + Uses the 'membrane_insertion' mechanism to generate species for + the protein before and after insertion. + + Returns + ------- + list of Species + List of species generated by the membrane_insertion mechanism, + including the protein and integrated product. + + """ mech_ins = self.get_mechanism('membrane_insertion') return mech_ins.update_species(self.membrane_protein, self.product) def update_reactions(self): + """Use 'membrane_insertion' to generate membrane insertion reactions. + + Uses the 'membrane_insertion' mechanism to generate reactions for + protein integration into the membrane. + + Returns + ------- + list of Reaction + List of reactions for protein insertion into the membrane. + + """ mech_ins = self.get_mechanism('membrane_insertion') return mech_ins.update_reactions( self.membrane_protein, @@ -207,13 +385,97 @@ def update_reactions(self): class MembraneChannel(Component): - """A class to represent membrane channels. - - The membrane channel transports substrates across the membrane - following the concentration gradient. Direction and mechanism will be - based on the specific transporter. - - Uses a mechanism called "transport". + """Membrane channel for facilitated transport across membranes. + + A `MembraneChannel` component represents a membrane channel or + transporter that facilitates substrate movement across a membrane + following concentration gradients. The direction of transport depends + on the specific transporter type. The component uses a 'transport' + mechanism to generate transport reactions. + + Parameters + ---------- + integral_membrane_protein : Species, str, or Component + The integral membrane protein that forms the channel. Can be a + `Species` object, string name, or `Component`. If a string, + automatically creates a protein species with appropriate direction + attribute. + substrate : Species, str, or Component + The substrate to be transported through the channel. Can be a + `Species` object, string name, or `Component`. + direction : str, optional + Direction of transport. If None, extracted from + integral_membrane_protein attributes. Common values: 'Importer' + (external --> internal), 'Exporter' (internal --> external), + 'Passive' (bidirectional). + internal_compartment : str or Compartment, default='Internal' + The internal compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + external_compartment : str or Compartment, default='External' + The external compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + attributes : list of str, optional + List of attribute tags to associate with substrate species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + integral_membrane_protein : Species + The membrane channel protein species. + substrate : Species + The substrate species in the source compartment (depends on + direction). + product : Species + The same substrate in the destination compartment. + + See Also + -------- + IntegralMembraneProtein : Protein insertion into membranes. + MembranePump : ATP-dependent active transport. + DiffusibleMolecule : Passive diffusion without channels. + Component : Base class for biomolecular components. + + Notes + ----- + The transport mechanism generates reactions based on the direction: + + - 'Importer': substrate_external + channel + --> substrate_internal + channel + - 'Exporter': substrate_internal + channel + --> substrate_external + channel + - 'Passive': bidirectional transport following gradients + + The component name is automatically generated as: + '_' + + Examples + -------- + Create a glucose importer: + + >>> importer = bcp.MembraneChannel( + ... integral_membrane_protein='GlucoseTransporter', + ... substrate='Glucose', + ... direction='Importer' + ... ) + + Create a passive channel: + + >>> channel = bcp.MembraneChannel( + ... integral_membrane_protein='WaterChannel', + ... substrate='Water', + ... direction='Passive' + ... ) + + Use with a mixture: + + >>> mixture = bcp.Mixture( + ... components=[importer], + ... mechanisms={'transport': bcp.Facilitated_Transport_MM()}, + ... parameter_file='mechanisms/transport_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ @@ -225,19 +487,8 @@ def __init__( internal_compartment: Union[str, Compartment] = 'Internal', external_compartment: Union[str, Compartment] = 'External', attributes=None, - **keywords, + **kwargs, ): - """Initialize a MembraneChannel object. - - :param substrate: substrate to be transported (str, Species, - Component) - :param direction: direction of transport based on transporter action - :param internal_compartment: name of internal compartment - :param external_compartment: name of external compartment - :param attributes: Species attribute - :param keywords: pass into the parent's (Component) initializer - - """ # Creates compartment object if compartment is a str if isinstance(internal_compartment, str): internal_compartment = Compartment(name=internal_compartment) @@ -245,6 +496,7 @@ def __init__( external_compartment = Compartment(name=external_compartment) # Set up the integral membrane protein + # TODO: allow integral_membrane_protein to be a Component if isinstance(integral_membrane_protein, str): integral_membrane_protein = self.set_species( integral_membrane_protein, @@ -313,18 +565,48 @@ def __init__( + self.integral_membrane_protein.compartment.name ) - Component.__init__(self=self, name=name, **keywords) + Component.__init__(self=self, name=name, **kwargs) def get_species(self): + """Get the integral membrane protein species. + + Returns + ------- + Species + The integral membrane protein species that forms the channel. + + """ return self.integral_membrane_protein def update_species(self): + """Use 'transport' mechanism to generate channel-mediated species. + + Uses the 'transport' mechanism to generate species including the + channel protein, substrate, and product. + + Returns + ------- + list of Species + List of species generated by the transport mechanism. + + """ mech_tra = self.get_mechanism('transport') return mech_tra.update_species( self.integral_membrane_protein, self.substrate, self.product ) def update_reactions(self): + """Use 'transport' mechanism to generate channel-mediated reactions. + + Uses the 'transport' mechanism to generate reactions for substrate + transport through the channel. + + Returns + ------- + list of Reaction + List of transport reactions through the membrane channel. + + """ mech_tra = self.get_mechanism('transport') return mech_tra.update_reactions( self.integral_membrane_protein, @@ -336,11 +618,105 @@ def update_reactions(self): class MembranePump(Component): - """A class to represent membrane pumps or transporters that require ATP. - - The membrane pump transports substrates unidirectionally across the - membrane, independent of the concentration gradient. Uses a mechanism - called 'transport'. + """ATP-dependent membrane pump for active transport. + + A `MembranePump` component represents an active transporter or pump + that uses ATP to transport substrates across membranes against + concentration gradients. The pump operates unidirectionally and requires + energy in the form of ATP. The component uses a 'transport' mechanism + to generate ATP-dependent transport reactions. + + Parameters + ---------- + membrane_pump : Species, str, or Component + The membrane pump protein species. Can be a `Species` object, + string name, or `Component`. If a string, automatically creates a + protein species with appropriate direction attribute. + substrate : Species, str, or Component + The substrate to be transported by the pump. Can be a `Species` + object, string name, or `Component`. + direction : str, optional + Direction of active transport. Common values: 'Importer' + (external --> internal), 'Exporter' (internal --> external), + 'Passive' (default). Affects substrate and ATP compartment + placement. + internal_compartment : str or Compartment, default='Internal' + The internal compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + external_compartment : str or Compartment, default='External' + The external compartment. Can be a string name (creates new + Compartment) or an existing `Compartment` object. + ATP : int, optional + Number of ATP molecules required per transport cycle. Default is 1. + attributes : list of str, optional + List of attribute tags to associate with substrate species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + membrane_pump : Species + The membrane pump protein species. + substrate : Species + The substrate species in the source compartment. + product : Species + The same substrate in the destination compartment. + energy : Species + ATP species used for energy (compartment depends on direction). + waste : Species + ADP species produced (compartment depends on direction). + + See Also + -------- + MembraneChannel : Facilitated transport without ATP. + DiffusibleMolecule : Passive diffusion. + Component : Base class for biomolecular components. + + Notes + ----- + Active transport requires ATP hydrolysis and can move substrates + against concentration gradients. The typical reaction scheme is: + + - Exporter: substrate_internal + ATP + pump --> + substrate_external + ADP + pump + - Importer: substrate_external + ATP + pump --> + substrate_internal + ADP + pump + + The ATP parameter controls the stoichiometry of ATP consumption per + transport event. + + The component name is automatically generated as: + '_' + + Examples + -------- + Create a simple ATP-dependent exporter: + + >>> pump = bcp.MembranePump( + ... membrane_pump='CalciumPump', + ... substrate='Calcium', + ... direction='Exporter', + ... ATP=2 + ... ) + + Create an ABC transporter (importer): + + >>> abc = bcp.MembranePump( + ... membrane_pump='ABC_Transporter', + ... substrate='Maltose', + ... direction='Importer', + ... ATP=1 + ... ) + + Use with a mixture: + + >>> mixture = bcp.Mixture( + ... components=[pump], + ... mechanisms={'transport': bcp.Primary_Active_Transport_MM()}, + ... parameter_file='mechanisms/transport_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ @@ -353,19 +729,8 @@ def __init__( external_compartment: Union[str, Compartment] = 'External', ATP: int = None, attributes=None, - **keywords, + **kwargs, ): - """Initialize a MembranePump object. - - :param substrate: name of the substrate, reference to a Species - or Component - :param direction: give direction of transport ref to vesicle - :param internal_compartment: name of internal compartment - :param external_compartment: name of external compartment - :param ATP: indicates the number of ATP required for transport - :param attributes: Species attribute - :param keywords: pass into the parent's (Component) initializer - """ # Creates compartment object if compartment is a str if isinstance(internal_compartment, str): internal_compartment = Compartment(name=internal_compartment) @@ -498,12 +863,32 @@ def __init__( + self.membrane_pump.compartment.name ) - Component.__init__(self=self, name=name, **keywords) + Component.__init__(self=self, name=name, **kwargs) def get_species(self): + """Get the membrane pump protein species. + + Returns + ------- + Species + The membrane pump protein species. + + """ return self.membrane_pump def update_species(self): + """Use 'trasnport' mechanism to generate ATP-dependent species. + + Uses the 'transport' mechanism to generate species including the + pump protein, substrate, product, ATP, and ADP. + + Returns + ------- + list of Species + List of species generated by the transport mechanism, + including pump, substrate, product, energy, and waste. + + """ mech_cat = self.get_mechanism('transport') return mech_cat.update_species( self.membrane_pump, @@ -514,6 +899,17 @@ def update_species(self): ) def update_reactions(self): + """Use 'trasnport' mechanism to generate ATP-dependent reactions. + + Uses the 'transport' mechanism to generate reactions for active + transport coupled to ATP hydrolysis. + + Returns + ------- + list of Reaction + List of ATP-dependent transport reactions. + + """ mech_cat = self.get_mechanism('transport') return mech_cat.update_reactions( self.membrane_pump, @@ -527,11 +923,116 @@ def update_reactions(self): class MembraneSensor(Component): - """A class to represent a two-component system (TCS) membrane sensor. - - The membrane sensor protein senses the signal substrate and added the - assigned substrate to the response protein. Uses a mechanism called - 'membrane_sensor'. + """Two-component system (TCS) membrane sensor protein. + + A `MembraneSensor` component represents a membrane sensor protein in a + two-component signaling system. The sensor detects external signal + substrates and catalyzes the transfer of a chemical group (typically + phosphate) to a response protein, activating it. The component uses a + 'membrane_sensor' mechanism to generate signal transduction reactions. + + Parameters + ---------- + membrane_sensor_protein : Species, str, or Component + The membrane sensor protein (histidine kinase) that detects the + signal. Can be a `Species` object, string name, or `Component`. + response_protein : Species, str, or Component + The cytoplasmic response regulator protein that receives the + signal. Can be a `Species` object, string name, or `Component`. + assigned_substrate : Species, str, or Component + The chemical group to be transferred (typically phosphate). Can be + a `Species` object, string name, or `Component`. + signal_substrate : Species, str, or Component + The external signal molecule that activates the sensor. Can be a + `Species` object, string name, or `Component`. + product : Species, str, or Component, optional + The activated response protein product. If None, automatically + named as 'active'. + internal_compartment : str or Compartment, default='Internal' + The internal compartment containing response protein. Can be a + string name (creates new Compartment) or an existing `Compartment` + object. + external_compartment : str or Compartment, default='External' + The external compartment containing signal. Can be a string name + (creates new Compartment) or an existing `Compartment` object. + ATP : int, default=2 + Number of ATP molecules required for the signaling process. + attributes : list of str, optional + List of attribute tags to associate with species. + **kwargs + Additional keyword arguments passed to the `Component` base class + constructor. + + Attributes + ---------- + membrane_sensor_protein : Species + The membrane sensor protein species. + response_protein : Species + The response regulator protein species. + assigned_substrate : Species + The substrate to be transferred (e.g., phosphate). + signal_substrate : Species + The external signal molecule species. + product : Species + The activated response protein species. + energy : Species + ATP species used for energy. + waste : Species + ADP species produced. + + See Also + -------- + Component : Base class for biomolecular components. + + Notes + ----- + Two-component systems (TCS) are common bacterial signal transduction + pathways. The typical mechanism involves: + + 1. Signal detection by membrane sensor (histidine kinase) + 2. Autophosphorylation of sensor using ATP + 3. Phosphotransfer to response regulator + 4. Activated response regulator regulates gene expression + + The general reaction scheme: + + signal + sensor + ATP + response_protein --> + signal + sensor + ADP + response_protein-P + + The component name is automatically generated as: + '_' + + Examples + -------- + Create a simple two-component system: + + >>> tcs = bcp.MembraneSensor( + ... membrane_sensor_protein='EnvZ', + ... response_protein='OmpR', + ... assigned_substrate='Phosphate', + ... signal_substrate='Osmolarity', + ... ATP=2 + ... ) + + Create a chemotaxis receptor: + + >>> chemoreceptor = bcp.MembraneSensor( + ... membrane_sensor_protein='CheA', + ... response_protein='CheY', + ... assigned_substrate='Phosphate', + ... signal_substrate='Aspartate', + ... product='CheY_P' + ... ) + + Use with a mixture: + + >>> mixture = bcp.Mixture( + ... components=[tcs], + ... mechanisms={ + ... 'membrane_sensor': bcp.Membrane_Signaling_Pathway_MM()}, + ... parameter_file='mechanisms/transport_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ @@ -546,26 +1047,8 @@ def __init__( external_compartment: Union[str, Compartment] = 'External', ATP: int = 2, attributes=None, - **keywords, + **kwargs, ): - """Initialize a MembraneSensor object. - - :param membrane_sensor_protein: name of the membrane protein in - the TCS, reference to an Species or Component - :param response_protein: name of the response protein in the TCS, - reference to an Species or Component - :param assigned_substrate: name of the assigned substrate in the TCS, - reference to an Species or Component - :param signal_substrate: name of the signal substrate in the TCS, - reference to an Species or Component - :param product: name of the product in the TCS, reference to an - Species or Component - :param internal_compartment: name of internal compartment - :param external_compartment: name of external compartment - :param ATP: indicates the number of ATP required for transport - :param attributes: Species attribute - :param keywords: pass into the parent's (Component) initializer - """ # Creates compartment object if compartment is a str if isinstance(internal_compartment, str): internal_compartment = Compartment(name=internal_compartment) @@ -649,12 +1132,32 @@ def __init__( + self.membrane_sensor_protein.compartment.name ) - Component.__init__(self=self, name=name, **keywords) + Component.__init__(self=self, name=name, **kwargs) def get_species(self): + """Get the membrane sensor protein species. + + Returns + ------- + Species + The membrane sensor protein (histidine kinase) species. + + """ return self.membrane_sensor_protein def update_species(self): + """Use 'membrane_sensor' to generate species signaling species. + + Uses the 'membrane_sensor' mechanism to generate all species + involved in the signaling pathway including sensor, response + protein, substrates, signal, product, ATP, and ADP. + + Returns + ------- + list of Species + List of species generated by the membrane_sensor mechanism. + + """ mech_sen = self.get_mechanism('membrane_sensor') return mech_sen.update_species( self.membrane_sensor_protein, @@ -667,6 +1170,19 @@ def update_species(self): ) def update_reactions(self): + """Use 'membrane_sensor' to generate species signaling reactions. + + Uses the 'membrane_sensor' mechanism to generate reactions for + signal detection, ATP-dependent phosphorylation, and + phosphotransfer to the response regulator. + + Returns + ------- + list of Reaction + List of signal transduction reactions including sensing, + autophosphorylation, and phosphotransfer. + + """ mech_sen = self.get_mechanism('membrane_sensor') return mech_sen.update_reactions( self.membrane_sensor_protein, diff --git a/biocrnpyler/core/chemical_reaction_network.py b/biocrnpyler/core/chemical_reaction_network.py index 47c0c5e6..9e2e9f06 100644 --- a/biocrnpyler/core/chemical_reaction_network.py +++ b/biocrnpyler/core/chemical_reaction_network.py @@ -26,20 +26,105 @@ class ChemicalReactionNetwork(object): - r"""Network of reactions between a set of species. - - A chemical reaction network is a container of species and reactions - chemical reaction networks can be compiled into SBML. - - reaction types: - mass action: standard mass action semantics where the propensity of a - reaction is given by deterministic propensity = - .. math:: - k \Prod_{inputs i} [S_i]^a_i - stochastic propensity = - .. math:: - k \Prod_{inputs i} (S_i)!/(S_i - a_i)! - where a_i is the spectrometric coefficient of species i + r"""Container for chemical species and their reactions. + + A ChemicalReactionNetwork (CRN) represents a biochemical system as a set + of species and reactions between them. CRNs can be compiled to SBML format + for simulation with various tools, or simulated directly with bioscrape or + roadrunner. + + Parameters + ---------- + species : list of Species + List of chemical species in the network. + reactions : list of Reaction + List of reactions between species. Each reaction specifies inputs, + outputs, and rate parameters. + initial_concentration_dict : dict, optional + Dictionary mapping Species to their initial concentrations. Values can + be numbers or `Parameter` objects. If None, an empty dictionary is + created. + show_warnings : bool, default=False + If True, shows warnings about duplicate species/reactions or + inconsistencies during CRN validation. + + Attributes + ---------- + species : list of Species + Deep copy of the species list (use `add_species` to modify). + reactions : list of Reaction + Deep copy of the reactions list (use `add_reactions` to modify). + initial_concentration_dict : dict + Dictionary of initial concentrations for species in the CRN. + + See Also + -------- + Species : Chemical species in a CRN. + Reaction : Chemical reaction between species. + Mixture : High-level interface for building CRNs from components. + + Notes + ----- + Mass action reactions follow standard mass action kinetics: + + - Deterministic propensity: :math:`k \prod_{i} [S_i]^{a_i}` + - Stochastic propensity: :math:`k \prod_{i} \frac{S_i!}{(S_i - a_i)!}` + + where :math:`a_i` is the stoichiometric coefficient of species :math:`i`. + + A valid CRN requires: + + - All species in reactions must be in the species list + - All species and reactions must be unique (duplicates trigger warnings) + - Initial concentrations must be non-negative + + Once created, species and reactions cannot be removed, only added. This + ensures CRN validity is maintained throughout its lifetime. + + Chemical reaction networks can be simulated by writing the output + as SMBL using `write_sbml_file` and then loading into an external + simulator, or by using the bioscrape package, which can be called + directly using `simulate_with_bioscrape_via_sbml`. + + Examples + -------- + Create a simple CRN manually: + + >>> # Define species + >>> S = bcp.Species('S') + >>> P = bcp.Species('P') + >>> E = bcp.Species('protein_E') + >>> C = bcp.Species('C') + >>> # Define reactions + >>> rxn1 = bcp.Reaction.from_massaction( + ... [S, E], [C], k_forward=0.1, k_reverse=1e-4) + >>> rxn2 = bcp.Reaction.from_massaction([C], [E, P], k_forward=0.01) + >>> # Create CRN + >>> crn = bcp.ChemicalReactionNetwork( + ... species=[S, E, C, P], + ... reactions=[rxn1, rxn2] + ... ) + + Compile a CRN from a mixture: + + >>> enzyme = bcp.Enzyme('E', 'S', 'P') + >>> mixture = bcp.Mixture( + ... components=[enzyme], + ... mechanisms={'catalysis': bcp.MichaelisMenten()}, + ... parameters={'kb': 0.1, 'ku': 1e-4, 'kcat': 0.01}) + >>> crn = mixture.compile_crn() + >>> print( + ... f"CRN has {len(crn.species)} species and " + ... f"{len(crn.reactions)} reactions") + CRN has 4 species and 2 reactions + + Export to SBML and simulate: + + >>> crn.write_sbml_file('model.xml') + >>> result = crn.simulate_with_bioscrape_via_sbml( + ... initial_condition_dict={S: 100, 'protein_E': 50, P: 0}, + ... timepoints=np.linspace(0, 5)) + """ def __init__( @@ -66,25 +151,30 @@ def __init__( @property def species(self): + """list: List of Species : Deep copy of all species in the CRN.""" return copy.deepcopy(self._species) @species.setter def species(self, species): - """Sets the species of the CRN object. - - If the species is not set, it initializes an empty list and adds - the species to it. If the species is already set, it raises an - AttributeError. A _species_set is used to ensure that no duplicate - species are added to the CRN. In earlier BioCRNPyler versions, a - _species_dict was used. This dictionary had key as species, and - value as "True", which is unintuitive. Since, sets are designed to - keep unique elements, so we use a set to keep track of species. - - Args: - species (_type_): _description_ - - Raises: - AttributeError: _description_ + """Set the initial species list for the CRN. + + Parameters + ---------- + species : list of Species + Initial species to add to the CRN. Additional species can be added + later using `add_species`. + + Raises + ------ + AttributeError + If species list is already set. Species cannot be removed once + added, only new species can be added with `add_species`. + + Notes + ----- + This setter can only be called once during CRN initialization. A + `_species_set` is maintained internally to ensure no duplicate species + are added to the CRN. """ if not hasattr(self, '_species'): @@ -99,10 +189,26 @@ def species(self, species): @property def reactions(self): + """List of Reaction: Deep copy of all reactions in the CRN.""" return copy.deepcopy(self._reactions) @reactions.setter def reactions(self, reactions): + """Set the initial reactions list for the CRN. + + Parameters + ---------- + reactions : list of Reaction + Initial reactions to add to the CRN. Additional reactions can be + added later using `add_reactions`. + + Raises + ------ + AttributeError + If reactions list is already set. Reactions cannot be removed once + added, only new reactions can be added with `add_reactions`. + + """ if not hasattr(self, '_reactions'): self._reactions = [] self.add_reactions(reactions) @@ -113,11 +219,30 @@ def reactions(self, reactions): ) def add_species(self, species, copy_species=True, compartment=None): - """Adds a Species or a list of Species to the CRN object. - - :param species: Species instance or list of Species instances - :param copy_species: whether to deep copy Species added to the CRN. - Protects CRN validity at teh expense of speed. + """Add species to the CRN. + + Parameters + ---------- + species : Species or list of Species + Species object(s) to add to the CRN. Lists are automatically + flattened and binding locations are removed. + copy_species : bool, default=True + If True, deep-copies species before adding them to the CRN. + Protects CRN validity at the expense of speed. + compartment : Compartment, optional + If provided, assigns this compartment to any species with default + compartments. + + Raises + ------ + ValueError + If any element is not a Species object. + + Notes + ----- + Duplicate species (based on equality) are automatically filtered out. + Species are stored in both a list (`_species`) and a set + (`_species_set`) for efficient duplicate checking. """ if not isinstance(species, list): @@ -152,15 +277,34 @@ def add_reactions( add_species=True, compartment=None, ) -> None: - """Adds a reaction or a list of reactions to the CRN object. - - :param reactions: Reaction instance or list of Reaction instances - :param copy_reactions: whether to deep copy reactions before adding - them to the CRN. Protects CRN validity at the - expense of speed. - :param add_species: whether to add species in reactions to the CRN. - Prevents errors at the expense of speed. - :return: None + """Add reactions to the CRN. + + Parameters + ---------- + reactions : Reaction or list of Reaction + Reaction object(s) to add to the CRN. + copy_reactions : bool, default=True + If True, deep-copies reactions before adding them to the CRN. + Protects CRN validity at the expense of speed. + add_species : bool, default=True + If True, automatically adds any species appearing in the reactions + to the CRN. Prevents missing species errors at the expense of + speed. + compartment : Compartment, optional + If provided, assigns this compartment to any species with default + compartments found in the reactions. + + Raises + ------ + ValueError + If any element is not a Reaction object. + + Notes + ----- + Unlike species, reactions are not checked for duplicates when added. + It is recommended to keep `copy_reactions=True` to protect the CRN + from external modifications. + """ if not isinstance(reactions, list): reactions = [reactions] @@ -197,10 +341,27 @@ def add_reactions( @property def initial_concentration_dict(self): + """dict: Dictionary mapping Species to initial concentrations.""" return self._initial_concentration_dict @initial_concentration_dict.setter def initial_concentration_dict(self, initial_concentration_dict): + """Set initial concentrations for species in the CRN. + + Parameters + ---------- + initial_concentration_dict : dict or None + Dictionary mapping Species objects to their initial + concentrations. Values can be numbers or `Parameter` objects. If + None, an empty dictionary is created. + + Raises + ------ + ValueError + If a species in the dictionary is not in the CRN, or if any + concentration is negative. + + """ if initial_concentration_dict is None: self._initial_concentration_dict = {} elif isinstance(initial_concentration_dict, dict): @@ -224,13 +385,38 @@ def initial_concentration_dict(self, initial_concentration_dict): def check_crn_validity( reactions: List[Reaction], species: List[Species], show_warnings=True ) -> Tuple[List[Reaction], List[Species]]: - """Checks that lists of reactions of species can form a valid CRN. + """Validate that reactions and species can form a valid CRN. + + Checks for duplicate species/reactions and verifies that all species + in reactions are present in the species list. + + Parameters + ---------- + reactions : list of Reaction + List of reactions to validate. + species : list of Species + List of species to validate. + show_warnings : bool, default=True + If True, issues warnings for duplicates or inconsistencies. + + Returns + ------- + tuple of (list of Reaction, list of Species) + The input reactions and species lists, unchanged. + + Raises + ------ + ValueError + If any reaction is not a Reaction object, or any species is not a + Species object. + + Warns + ----- + UserWarning + - Duplicate reactions or species are found + - Species exist without reactions + - Reactions contain unlisted species - :param reactions: list of reaction - :param species: list of species - :param show_warnings: whether to show warning when duplicated - reactions/species was found - :return: tuple(reaction,species) """ if not all(isinstance(r, Reaction) for r in reactions): raise ValueError("A non-reaction object was used as a reaction!") @@ -298,13 +484,40 @@ def pretty_print( show_compartment=False, **kwargs, ): - """A more powerful printing function. + """Generate detailed, human-readable string representation of the CRN. + + Parameters + ---------- + show_rates : bool, default=True + If True, displays reaction rate functions and parameters. + show_material : bool, default=True + If True, displays species material types (e.g., 'dna', 'protein'). + show_attributes : bool, default=True + If True, displays species attributes. + show_initial_concentration : bool, default=True + If True, displays initial concentrations for each species. + show_keys : bool, default=True + If True, shows parameter database keys for initial concentrations + (useful for debugging parameter lookup). + show_compartment : bool, default=False + If True, displays compartment information for each species. + **kwargs + Additional keyword arguments passed to species and reaction + `pretty_print` methods. + + Returns + ------- + str + Formatted string with species (sorted by initial concentration) + and reactions with detailed information. + + Notes + ----- + This method provides much more detailed output than `__repr__`, + making it useful for debugging and understanding CRN structure. + Species are sorted by initial concentration (highest first) for easier + analysis. - Useful for understanding CRNs but does not return string identifiers. - `show_material` toggles whether species.material is printed. - `show_attributes` toggles whether species.attributes is printed - `show_rates` toggles whether reaction rate functions are printed - `show_compartment` toggles whether species.compartment is printed """ txt = 'Species' + f"(N = {len(self._species)}) = " + '{\n' @@ -369,6 +582,21 @@ def ics(s): def initial_condition_vector( self, init_cond_dict: Union[Dict[str, float], Dict[Species, float]] ): + """Generate an initial condition vector for simulations. + + Parameters + ---------- + init_cond_dict : dict + Dictionary mapping species (or species names as strings) to their + initial concentrations. + + Returns + ------- + list of float + Vector of initial concentrations matching the order of species in + `self._species`. Species not in `init_cond_dict` are set to 0.0. + + """ x0 = [0.0] * len(self._species) for idx, s in enumerate(self._species): if s in init_cond_dict: @@ -378,7 +606,44 @@ def initial_condition_vector( def get_all_species_containing( self, species: Species, return_as_strings=False ): - """Return all species (complexes) containing given species.""" + """Find all species (complexes) that contain a given species. + + Searches recursively through all species in the CRN to find those that + contain the target species as a component. + + Parameters + ---------- + species : Species + The species to search for within other species. + return_as_strings : bool, default=False + If True, returns species as string representations. If False, + returns actual Species objects. + + Returns + ------- + list + List of Species objects (or strings if `return_as_strings=True`) + that contain the target species. + + Raises + ------ + ValueError + If `species` is not a Species object. + + Examples + -------- + >>> substrate = bcp.Species('S') + >>> enzyme = bcp.Enzyme('E', substrate, 'P') + >>> mixture = bcp.Mixture( + ... components=[enzyme], + ... mechanisms={'catalysis': bcp.MichaelisMenten()}, + ... parameters={'kb': 0.1, 'ku': 1e-4, 'kcat': 0.01}) + >>> crn = mixture.compile_crn() + >>> # Find all complexes containing S + >>> crn.get_all_species_containing(substrate) + [S, complex_S_protein_E_] + + """ return_list = [] if not isinstance(species, Species): raise ValueError( @@ -394,9 +659,34 @@ def get_all_species_containing( return return_list def replace_species(self, species: Species, new_species: Species): - """Replaces species with new_species in the entire CRN. + """Replace a species with another throughout the CRN. + + Creates a new CRN where all occurrences of a target species are + replaced with a new species. The original CRN is not modified. + + Parameters + ---------- + species : Species + The species to be replaced. + new_species : Species + The species to replace with. + + Returns + ------- + ChemicalReactionNetwork + New CRN with the species replacement applied. + + Raises + ------ + ValueError + If either argument is not a Species object. + + Notes + ----- + This method does not modify the original CRN. It creates and returns + a new CRN with the replacement applied throughout all species and + reactions. - Does not act in place: returns a new CRN. """ if not isinstance(species, Species): raise ValueError( @@ -425,28 +715,54 @@ def generate_sbml_model( stochastic_model=False, show_warnings=False, check_validity=True, - **keywords, + **kwargs, ): - """Create new SBML model and populate with CRN species and reactions. + """Generate an SBML model from the CRN. + + Creates SBML document and model objects containing all species, + reactions, compartments, and parameters from the CRN. + + Parameters + ---------- + stochastic_model : bool, default=False + If True, generates an SBML model configured for stochastic + simulation. + show_warnings : bool, default=False + If True, shows warnings from CRN validity checking. + check_validity : bool, default=True + If True, validates the CRN before generating SBML. + **kwargs + Additional keyword arguments passed to `create_sbml_model` and + `add_all_reactions`. + + Returns + ------- + tuple of (libsbml.SBMLDocument, libsbml.Model) + The SBML document and model objects. The document can be written + to a file or further manipulated. + + Warns + ----- + UserWarning + Issues a warning if the generated SBML model contains errors. + + See Also + -------- + write_sbml_file : Write the SBML model directly to a file. - :param stochastic_model: whether the model is stochastic - :param show_warnings: of from check crn validity - :param keywords: extra keywords pass onto create_sbml_model() and - add_all_reactions() - :return: tuple: (document,model) SBML objects """ if check_validity: ChemicalReactionNetwork.check_crn_validity( self._reactions, self._species, show_warnings=show_warnings ) - document, model = create_sbml_model(**keywords) + document, model = create_sbml_model(**kwargs) all_compartments = [] for species in self._species: if species.compartment not in all_compartments: all_compartments.append(species.compartment) add_all_compartments( - model=model, compartments=all_compartments, **keywords + model=model, compartments=all_compartments, **kwargs ) add_all_species( @@ -458,7 +774,7 @@ def generate_sbml_model( model=model, reactions=self._reactions, stochastic_model=stochastic_model, - **keywords, + **kwargs, ) if document.getNumErrors(): @@ -473,20 +789,44 @@ def write_sbml_file( file_name=None, stochastic_model=False, check_validity=True, - **keywords, + **kwargs, ) -> bool: - """Writes CRN object to a SBML file. + """Write the CRN to an SBML file. + + Generates an SBML model from the CRN and writes it to a file for use + with simulators like COPASI, VCell, or bioscrape. + + Parameters + ---------- + file_name : str + Path where the SBML file will be written. + stochastic_model : bool, default=False + If True, exports an SBML file configured for stochastic + simulations. + check_validity : bool, default=True + If True, validates the CRN before generating SBML. + **kwargs + Additional keyword arguments passed to `generate_sbml_model`. + + Returns + ------- + bool + True if the file was written successfully. + + See Also + -------- + generate_sbml_model : Generate SBML objects without writing to file. + + Examples + -------- + >>> crn.write_sbml_file('my_model.xml') + >>> crn.write_sbml_file('stochastic_model.xml', stochastic_model=True) - :param file_name: name of the file where the SBML model gets written - :param stochastic_model: export an SBML file which ready for - stochastic simulations - :param keywords: keywords that passed into generate_sbml_model() - :return: bool, show whether the writing process was successful """ document, _ = self.generate_sbml_model( stochastic_model=stochastic_model, check_validity=check_validity, - **keywords, + **kwargs, ) sbml_string = libsbml.writeSBMLToString(document) with open(file_name, 'w') as f: @@ -501,11 +841,34 @@ def simulate_with_bioscrape( return_dataframe=True, safe=False, ): - """Simulate CRN model with bioscrape. - - [Bioscrape on GitHub](https://github.com/biocircuits/bioscrape). + """Simulate CRN with bioscrape. + + .. deprecated:: 1.0.0 + This method is deprecated. Use + `simulate_with_bioscrape_via_sbml` instead. + + Parameters + ---------- + timepoints : array-like + Time points for simulation. + initial_condition_dict : dict, optional + Dictionary of initial concentrations. + stochastic : bool, default=False + If True, runs stochastic simulation. + return_dataframe : bool, default=True + If True, returns results as pandas DataFrame. + safe : bool, default=False + Safe mode for bioscrape simulation. + + Returns + ------- + DataFrame or array + Simulation results. + + See Also + -------- + simulate_with_bioscrape_via_sbml : Recommended simulation method. - Returns the data for all species as Pandas dataframe. """ result = None warnings.warn( @@ -537,11 +900,67 @@ def simulate_with_bioscrape_via_sbml( check_validity=True, **kwargs, ): - """Simulate CRN model with bioscrape via temporary SBML file. + """Simulate CRN with bioscrape via SBML export. + + Exports the CRN to an SBML file and simulates it using the bioscrape + simulator. Bioscrape is a stochastic and deterministic simulator for + biological circuits. + + Parameters + ---------- + timepoints : array-like + Array of time points at which to record simulation results. + filename : str, optional + Path to save the SBML file. If None, creates a temporary file + 'temp_sbml_file.xml'. + initial_condition_dict : dict, optional + Dictionary mapping species to initial concentrations. Overrides + the CRN's `initial_concentration_dict`. + return_dataframe : bool, default=True + If True, returns results as a pandas DataFrame. If False, returns + a numpy array. + stochastic : bool, default=False + If True, runs stochastic (Gillespie) simulation. If False, runs + deterministic (ODE) simulation. + safe : bool, default=False + If True, uses bioscrape's safe mode which checks for errors. + return_model : bool, default=False + If True, returns a tuple of (results, bioscrape_model). If False, + returns only results. + check_validity : bool, default=True + If True, validates the CRN before generating SBML. + **kwargs + Additional keyword arguments. 'sbml_warnings' can be set to True + to show SBML parsing warnings. + + Returns + ------- + DataFrame or array, or tuple + Simulation results as DataFrame or array. If `return_model=True`, + returns tuple of (results, bioscrape Model object). + + Warns + ----- + UserWarning + Issues a warning if bioscrape is not installed. + + Notes + ----- + Requires bioscrape to be installed: `pip install bioscrape` + + Bioscrape GitHub: https://github.com/biocircuits/bioscrape + + Examples + -------- + >>> result = crn.simulate_with_bioscrape_via_sbml( + ... timepoints=np.linspace(0, 10, 100) + ... ) + >>> # Stochastic simulation + >>> result = crn.simulate_with_bioscrape_via_sbml( + ... timepoints=np.linspace(0, 10, 100), + ... stochastic=True + ... ) - [Bioscrape on GitHub](https://github.com/biocircuits/bioscrape). - - Returns the data for all species as Pandas dataframe. """ result = None m = None @@ -599,19 +1018,55 @@ def simulate_with_roadrunner( return_roadrunner=False, check_validity=True, ): - """To simulate using roadrunner. - - Arguments: - timepoints: The array of time points to run the simulation for. - initial_condition_dict: - - Returns the results array as returned by RoadRunner OR a - Roadrunner model object. - - Refer to the libRoadRunner simulator library documentation for - details on simulation results: https://libroadrunner.org/. - - NOTE : Needs roadrunner package installed to simulate. + """Simulate CRN with libRoadRunner. + + Converts the CRN to SBML in memory and simulates it using the + libRoadRunner deterministic simulator. libRoadRunner is a fast + SBML simulator for deterministic (ODE) simulation. + + Parameters + ---------- + timepoints : list of float + Array of time points at which to record simulation results. + initial_condition_dict : dict, optional + Dictionary mapping species names (strings) to initial + concentrations. Overrides the CRN's `initial_concentration_dict`. + return_roadrunner : bool, default=False + If True, returns the RoadRunner model object instead of simulation + results. Useful for advanced control and analysis. + check_validity : bool, default=True + If True, validates the CRN before generating SBML. + + Returns + ------- + array or RoadRunner + If `return_roadrunner=False`, returns simulation results as a + numpy array. If `return_roadrunner=True`, returns the RoadRunner + model object. + + Warns + ----- + UserWarning + Issues a warning if libroadrunner is not installed. + + Notes + ----- + Requires libroadrunner to be installed: `pip install libroadrunner` + + libRoadRunner documentation: https://libroadrunner.org/ + + Examples + -------- + >>> result = crn.simulate_with_roadrunner( + ... timepoints=np.linspace(0, 10, 100) + ... ) + >>> # Get RoadRunner object for advanced control + >>> rr = crn.simulate_with_roadrunner( + ... timepoints=np.linspace(0, 10, 100), + ... return_roadrunner=True + ... ) + >>> # Run parameter scan with RoadRunner + >>> result = rr.simulate(0, 10, 100) """ res_ar = None diff --git a/biocrnpyler/core/compartment.py b/biocrnpyler/core/compartment.py index de8674d7..39fbf23f 100644 --- a/biocrnpyler/core/compartment.py +++ b/biocrnpyler/core/compartment.py @@ -3,20 +3,91 @@ class Compartment: - """A formal Compartment object for a Species in a CRN. + """Spatial compartment for organizing species in a CRN model. - A Compartment must have a name. They may also have a spatial dimension - (such as 2 for two-dimensional, or 3 for three-dimensional) and the - size in litres. + Compartments represent physically distinct regions where chemical species + can exist, such as the cytoplasm, nucleus, extracellular space, or + organelles. Each compartment has a name, size, and spatial dimensionality. + Species in different compartments are treated as distinct, even if they + have the same molecular identity. - Note: The "default" keyword is reserved for BioCRNpyler allotting a - default compartment. Users must choose a different string. + Parameters + ---------- + name : str + Name of the compartment. Must consist of letters, numbers, or + underscores. Cannot contain double underscores, and cannot begin or + end with special characters. Must start with a letter. The name + 'default' is reserved by BioCRNpyler. + size : float or int, default=1e-6 + Size of the compartment in the units specified by `unit`. Default is + 1 microliter (1e-6 liters). + spatial_dimensions : int, default=3 + Number of spatial dimensions (0 for point, 1 for line, 2 for surface, + 3 for volume). Must be non-negative. + unit : str, optional + Unit identifier for the compartment size (e.g., 'L', 'mL', 'µL'). + Must be a supported unit in BioCRNpyler. See documentation for + supported units or add custom units in 'core/units.py'. - The unit attribute for Compartment can be used to set unit for - the Compartment size. Make sure that the string identifier used - for the unit is a supported unit in BioCRNpyler. Check the - documentation to find a list of supported units. Add your own - custom units in units.py if needed. + Attributes + ---------- + name : str + Name of the compartment. + size : float + Size of the compartment. + spatial_dimensions : int + Number of spatial dimensions. + unit : str or None + Unit identifier for the compartment size. + + Raises + ------ + TypeError + If `name` is None. + ValueError + If `name` is not a string, contains invalid characters, or if `size` + or `spatial_dimensions` are invalid. + + See Also + -------- + Species : Chemical species that can be assigned to compartments. + Mixture : Container that can have a default compartment. + + Notes + ----- + The reserved name 'default' is used internally by BioCRNpyler for species + that have not been explicitly assigned to a compartment. User-defined + compartments should use other names. + + Two compartments are considered equal if they have the same name. If two + compartments have the same name but different sizes or spatial dimensions, + a ValueError is raised to prevent inconsistencies. + + Examples + -------- + Create a cytoplasm compartment: + + >>> cytoplasm = bcp.Compartment( + ... name="cytoplasm", + ... size=1e-15, # 1 femtoliter (bacterial cell volume) + ... spatial_dimensions=3, + ... unit="L" + ... ) + + Create a membrane compartment (2D): + + >>> membrane = bcp.Compartment( + ... name="membrane", + ... size=1e-12, # 1 square micrometer + ... spatial_dimensions=2, + ... unit="m^2" + ... ) + + Use compartments with species: + + >>> species_cyto = bcp.Species("Protein_X", compartment=cytoplasm) + >>> species_mem = bcp.Species("Protein_X", compartment=membrane) + >>> species_cyto == species_mem # False - different compartments """ @@ -28,10 +99,28 @@ def __init__(self, name: str, size=1e-6, spatial_dimensions=3, unit=None): @property def name(self): + """str: Name of the compartment.""" return self._name @name.setter def name(self, name: str): + """Set the compartment name with validation. + + Parameters + ---------- + name : str + Name for the compartment. Must consist of letters, numbers, or + underscores. Cannot contain double underscores ('__'), and cannot + begin or end with underscores. Must start with a letter. + + Raises + ------ + TypeError + If `name` is None. + ValueError + If `name` is not a string or contains invalid characters. + + """ if name is None: raise TypeError("Compartment name must be a string.") elif isinstance(name, str): @@ -54,10 +143,29 @@ def name(self, name: str): @property def spatial_dimensions(self): + """int: Number of spatial dimensions. + + 0 for point, 1 for line, 2 for surface, 3 for volume. + + """ return self._spatial_dimensions @spatial_dimensions.setter def spatial_dimensions(self, spatial_dimensions: int): + """Set the spatial dimensions with validation. + + Parameters + ---------- + spatial_dimensions : int + Number of spatial dimensions. Must be a non-negative integer. + Common values: 0 (point), 1 (line), 2 (surface), 3 (volume). + + Raises + ------ + ValueError + If `spatial_dimensions` is not an integer or is negative. + + """ if not isinstance(spatial_dimensions, int): raise ValueError( "Compartment spatial dimension must be an integer." @@ -71,10 +179,25 @@ def spatial_dimensions(self, spatial_dimensions: int): @property def size(self): + """float: Size of compartment in units specified by unit attribute.""" return self._size @size.setter def size(self, size: float): + """Set the compartment size with validation. + + Parameters + ---------- + size : float or int + Size of the compartment. Must be non-negative. Units are specified + by the `unit` attribute. + + Raises + ------ + ValueError + If `size` is not a float or int, or is negative. + + """ if not isinstance(size, (float, int)): raise ValueError("Compartment size must be a float or int.") elif size < 0: @@ -84,10 +207,26 @@ def size(self, size: float): @property def unit(self): + """str: Unit identifier for compartment size (e.g., 'mL', 'uL').""" return self._unit @unit.setter def unit(self, unit: str): + """Set the unit identifier with validation. + + Parameters + ---------- + unit : str or None + Unit identifier for the compartment size. Must be a supported unit + in BioCRNpyler (e.g., 'L', 'mL', 'µL' for volumes). See + documentation for supported units. Can be None for unitless sizes. + + Raises + ------ + ValueError + If `unit` is not a string (when not None). + + """ if unit is not None: if not isinstance(unit, str): raise ValueError( @@ -99,14 +238,35 @@ def unit(self, unit: str): self._unit = None def __eq__(self, other): - """Check for equality of compartments. + """Check equality of compartments by name. + + Two compartments are considered equal if they have the same name. + If two compartments have the same name but different sizes or spatial + dimensions, a ValueError is raised to prevent inconsistencies. + + Parameters + ---------- + other : Compartment + Another compartment to compare with. - Overrides the default implementation. Two compartments are - equivalent if they have the same name, spatial dimension, and size + Returns + ------- + bool + True if compartments have the same name (and consistent + attributes), False otherwise. - :param other: Compartment instance + Raises + ------ + ValueError + If compartments have the same name but different sizes or spatial + dimensions. - :return: boolean + Notes + ----- + This comparison is based solely on the compartment name. If two + compartments share a name, they must also share the same physical + properties (size and spatial dimensions) to maintain model + consistency. """ if isinstance(other, Compartment) and self.name == other.name: diff --git a/biocrnpyler/core/component.py b/biocrnpyler/core/component.py index e5f25006..7033071a 100644 --- a/biocrnpyler/core/component.py +++ b/biocrnpyler/core/component.py @@ -14,12 +14,96 @@ class Component: - """Component class for core components. - - These subclasses of Component represent different kinds of biomolecules. - - This class must be Subclassed to provide functionality with the - functions get_species and get_reactions overwritten. + """Base class for biomolecular components in BioCRNpyler. + + Component subclasses represent different kinds of biomolecules such as + DNA, RNA, proteins, and complexes. Components interact with mechanism + objects to generate chemical reaction network (CRN) species and reactions + during compilation. This class must be subclassed to provide functionality + by overriding the `update_species` and `update_reactions` methods. + + Parameters + ---------- + name : str or Species + Name of the component. If a `Species` object is provided, its name + attribute will be used. + mechanisms : dict or list, optional + Custom mechanisms to override default mechanisms from the mixture. + Can be a dict with mechanism types (str) as keys and mechanism + objects as values, or a list of mechanism objects. + parameters : dict, optional + Dictionary of parameter values to add to the component's parameter + database. Keys follow the format (mechanism, part_id, param_name). + parameter_file : str, optional + Path to a parameter file (CSV or TSV format) to load into the + component's parameter database. + mixture : Mixture, optional + Reference to the `Mixture` object containing this component. The + mixture provides default mechanisms and parameters. + compartment : Compartment, optional + The `Compartment` object representing the physical location of this + component. + attributes : list of str, optional + List of attribute strings to tag the component and its associated + species. Attributes can be used for mechanism selection and species + filtering. + initial_concentration : float, optional + Initial concentration of the component's primary species. Must be + non-negative. This value is added to the parameter database with key + ('initial concentration', None, component.name). + initial_condition_dictionary : dict, optional + Dictionary mapping species (or species names) to initial concentration + values for components with multiple species. + + Attributes + ---------- + name : str + The name of the component. + mechanisms : dict + Dictionary of mechanisms specific to this component, keyed by + mechanism type (str). + mixture : Mixture or None + Reference to the mixture containing this component. + compartment : Compartment or None + The compartment containing this component. + attributes : list of str + List of attribute tags associated with this component. + parameter_database : ParameterDatabase + Database storing all parameters associated with this component. + initial_concentration : float or None + Initial concentration value for the component's primary species. + initial_condition_dictionary : dict + Dictionary of initial conditions for species generated by this + component. + + See Also + -------- + Mechanism : Base class for reaction generation schemas. + Mixture : Container for components, mechanisms, and global parameters. + Species : Represents chemical species in a CRN. + + Notes + ----- + This is an abstract base class. Direct instantiation is possible but + subclasses like `DNA`, `RNA`, `Protein`, or `DNAassembly` should be + used for specific biomolecular functionality. + + The parameter lookup hierarchy is: + + 1. Component.parameter_database + 2. Component.mixture.parameter_database (if mixture is set) + + Examples + -------- + Create a basic component with custom parameters: + + >>> comp = bcp.Component( + ... name='MyComponent', + ... parameters={'kb': 100, 'ku': 10}, + ... initial_concentration=50.0 + ... ) + >>> comp.name + 'MyComponent' """ @@ -37,18 +121,6 @@ def __init__( initial_concentration=None, initial_condition_dictionary=None, ): - """Initializes a Component object. - - :param name: - :param mechanisms: - :param parameters: - :param parameter_file: - :param mixture: - :param compartment: - :param attributes: - :param initial_concentration: - :param initial_condition_dictionary: - """ if mechanisms is None: self.mechanisms = {} else: @@ -117,18 +189,19 @@ def initial_concentration(self, initial_concentration): @property def compartment(self): - """The compartment of the Component. - - :return: Compartment - """ + """Compartment or None: The compartment containing this component.""" return self._compartment @compartment.setter def compartment(self, compartment: Compartment) -> None: - """Set the compartment of the Component. + """Set the compartment of the component. + + Parameters + ---------- + compartment : Compartment + The compartment object to assign to this component. Also updates + any internal species with default compartments. - :param compartment: - :return: None """ if compartment is not None and not isinstance( compartment, Compartment @@ -151,18 +224,31 @@ def compartment(self, compartment: Compartment) -> None: raise TypeError("The compartment must be a Compartment object") def set_mixture(self, mixture) -> None: - """Set the mixture the Component is in. + """Set the mixture containing this component. + + Parameters + ---------- + mixture : Mixture or None + The mixture object that contains this component and provides + default mechanisms and parameters. - :param mixture: - :return: None """ self.mixture = mixture # TODO implement as an abstractmethod def get_species(self) -> None: - """The subclasses should implement this method! + """Get the primary species associated with this component. + + Returns + ------- + None + Subclasses should override this method to return their primary + `Species` object. + + Notes + ----- + This is a placeholder that should be implemented by subclasses. - :return: None """ return None @@ -174,13 +260,35 @@ def set_species( compartment=None, attributes=None, ) -> Species: - """Set species from strings, species, or Components. + """Convert various inputs into Species objects. + + Parameters + ---------- + species : Species, str, Component, or list + The species to convert. Can be a `Species` object (returned + as-is), a string (creates new Species), a `Component` (extracts + its species), or a list of any of these types. + material_type : str, optional + Material type for the species (e.g., 'dna', 'rna', 'protein'). + Only used when creating new Species from strings. + compartment : Compartment, optional + Compartment to assign to the species. Only used when creating + new Species from strings. + attributes : list of str, optional + Attributes to assign to the species. Only used when creating + new Species from strings. + + Returns + ------- + Species or list of Species + The converted Species object(s). Returns a list if input was a + list. + + Raises + ------ + ValueError + If the input cannot be converted to a valid Species. - :param species: Species, str, Component - :param material_type: - :param compartment: - :param attributes: - :return: Species """ if isinstance(species, Species): return species @@ -217,11 +325,75 @@ def __hash__(self): return str.__hash__(repr(self.get_species())) def set_attributes(self, attributes: List[str]): + """Set multiple attributes for the component. + + Adds a list of attribute tags to the component and its associated + species by calling `add_attribute` for each attribute in the list. + + Parameters + ---------- + attributes : list of str or None + List of attribute strings to add to the component. If None, no + action is taken. + + See Also + -------- + add_attribute : Add a single attribute to the component. + + Examples + -------- + >>> comp = bcp.Protein(name="MyProtein") + >>> comp.set_attributes(["degtagged", "fluorescent"]) + >>> comp.attributes + ['degtagged', 'fluorescent'] + + """ if attributes is not None: for attribute in attributes: self.add_attribute(attribute) def add_attribute(self, attribute: str): + """Add a single attribute to the component. + + Adds an attribute tag to the component's attribute list and to its + associated species object, if one exists. Attributes can be used for + mechanism selection, species filtering, and tracking special + properties. + + Parameters + ---------- + attribute : str + Attribute string to add to the component. Must be a non-None + string value. + + Raises + ------ + AssertionError + If `attribute` is not a string or is None. + Warning + If the component has no internal species to which the attribute + can be added. + + Notes + ----- + Attributes are commonly used to tag components with properties such + as: + + - Degradation tags (e.g., 'degtagged', 'ssrAtagged', ) + - Functional properties (e.g., 'fluorescent', 'membranebound') + - Regulatory elements (e.g., 'inducible', 'repressible') + + Examples + -------- + Add attributes to tag a protein with special properties: + + >>> protein = bcp.Protein('GFP') + >>> protein.add_attribute('fluorescent') + >>> protein.add_attribute('ssrAtagged') + >>> protein.attributes + ['fluorescent', 'ssrAtagged'] + + """ assert ( isinstance(attribute, str) and attribute is not None ), f"Attribute: {attribute} must be a str" @@ -242,12 +414,20 @@ def update_parameters( parameter_database=None, overwrite_parameters=True, ): - """Updates the ParameterDatabase inside a Component. - - Possible inputs: - parameter_file (string) - parameters (dict) - parameter_database (ParameterDatabase) + """Update the parameter database with new parameters. + + Parameters + ---------- + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + parameters : dict, optional + Dictionary of parameters to add. Keys follow the format + (mechanism, part_id, param_name). + parameter_database : ParameterDatabase, optional + Another parameter database to merge into component's database. + overwrite_parameters : bool, default=True + If True, new parameter values overwrite existing ones. If False, + existing parameters are preserved. """ if parameter_file is not None: @@ -266,15 +446,32 @@ def update_parameters( ) def get_mechanism(self, mechanism_type, optional_mechanism=False): - """Searches the Component for a Mechanism of the correct type. - - If the Component does not have the mechanism, searches the - Components' Mixture for the Mechanism. - - :param mechanism_type: - :param optional_mechanism: toggles whether an error is thrown if - no mechanism is found - :return: + """Retrieve a mechanism by type from the component or its mixture. + + Searches first in the component's mechanism dictionary, then falls + back to the mixture's mechanisms if not found. + + Parameters + ---------- + mechanism_type : str + The type identifier of the mechanism to retrieve (e.g., + 'transcription', 'translation', 'binding'). + optional_mechanism : bool, default=False + If True, returns None when mechanism not found. If False, raises + KeyError when mechanism not found. + + Returns + ------- + Mechanism or None + The requested mechanism object, or None if not found and + `optional_mechanism` is True. + + Raises + ------ + TypeError + If `mechanism_type` is not a string. + KeyError + If mechanism not found and `optional_mechanism` is False. """ if not isinstance(mechanism_type, str): @@ -318,18 +515,30 @@ def add_mechanism( overwrite=False, optional_mechanism=False, ): - """Add a mechanism to the component mechanism dictionary. - - Adds a mechanism of type mech_type to the Component Mechanism - dictionary. - - :param mechanism: - :param mech_type: - :param overwrite: toggles whether the mechanism is added overwriting - any mechanism with the same key. - :param optional_mechanism: toggles whether an error is thrown if a - Mechanism is added that conflicts with an exising Mechanism - :return: + """Add a mechanism to this component's mechanism dictionary. + + Parameters + ---------- + mechanism : Mechanism + The mechanism object to add. + mech_type : str, optional + The type key under which to store the mechanism. If None, uses + the mechanism's `mechanism_type` attribute. + overwrite : bool, default=False + If True, replaces any existing mechanism with the same key. + If False, raises ValueError when key already exists. + optional_mechanism : bool, default=False + If True, suppresses the ValueError when a mechanism key conflict + occurs and `overwrite` is False. + + Raises + ------ + TypeError + If `mechanism` is not a Mechanism object, or if `mech_type` is + not a string. + ValueError + If mechanism key already exists, `overwrite` is False, and + `optional_mechanism` is False. """ if not hasattr(self, '_mechanisms'): @@ -361,14 +570,30 @@ def add_mechanisms( overwrite=False, optional_mechanism=False, ): - """Add a list or dictionary of mechanisms to the mixture. - - :param mechanisms: Can take both GlobalMechanisms and Mechanisms - :param overwrite: toggles whether the mechanism is added overwriting - any mechanism with the same key. - :param optional_mechanism: toggles whether an error is thrown if a - Mechanism is added that conflicts with an exising Mechanism - :return: + """Add multiple mechanisms to this component. + + Accepts mechanisms as a single object, list, or dictionary and adds + them to the component's mechanism dictionary. + + Parameters + ---------- + mechanisms : Mechanism, GlobalMechanism, dict, or list + The mechanism(s) to add. Can be a single mechanism, a dict with + mechanism types as keys and mechanisms as values, or a list of + mechanisms. + overwrite : bool, default=False + If True, replaces any existing mechanisms with the same keys. + If False, raises ValueError when keys already exist. + optional_mechanism : bool, default=False + If True, suppresses ValueError when mechanism key conflicts occur + and `overwrite` is False. + + Raises + ------ + ValueError + If `mechanisms` is not a valid type, or if mechanism key conflicts + occur with `overwrite=False` and `optional_mechanism=False`. + """ if isinstance(mechanisms, Mechanism): self.add_mechanism( @@ -406,22 +631,47 @@ def get_parameter( return_none=False, check_mixture=True, ) -> Union[Parameter, Real]: - """Get a parameter from different objects that hold parameters. - - Hierarchy: - 1. searches for the Parameter in Component.parameter_database - 2. searches for the parameter in Component.mixture.parameter_database - - :param param_name: - :param part_id: - :param mechanism: - :param return_numerical: numerical value or the parameter object is - returned - :param return_none: returns None instead of throwing an error if a - parameter isn't found - :param check_mixture: toggle whether or not to check the Component's - Mixture as well - :return: Parameter object or a Real number + """Retrieve parameter from component or mixture parameter database. + + Searches first in the component's parameter database, then falls back + to the mixture's parameter database if not found. + + Parameters + ---------- + param_name : str + Name of the parameter to retrieve. + part_id : str, optional + Part identifier for the parameter lookup key. + mechanism : str, optional + Mechanism identifier for the parameter lookup key. + return_numerical : bool, default=False + If True, returns the numerical value. If False, returns the + `Parameter` object. + return_none : bool, default=False + If True, returns None when parameter not found. If False, raises + ValueError when parameter not found. + check_mixture : bool, default=True + If True, searches the mixture's parameter database if not found + in the component's database. + + Returns + ------- + Parameter, Real, or None + The parameter object or its numerical value, or None if not found + and `return_none` is True. + + Raises + ------ + ValueError + If parameter not found and `return_none` is False. + + Notes + ----- + Parameter lookup follows the hierarchy: + + 1. Component.parameter_database + 2. Component.mixture.parameter_database (if `check_mixture` is True) + """ # Try the Component ParameterDatabase param = self.parameter_database.find_parameter( @@ -446,9 +696,19 @@ def get_parameter( # TODO implement abstractmethod def update_species(self) -> List[Species]: - """The subclasses should implement this method! + """Generate and return species associated with this component. + + Returns + ------- + list of Species + List of species objects generated by this component. This base + implementation returns an empty list. + + Notes + ----- + This method should be overridden by subclasses to return the actual + species generated by the component during CRN compilation. - :return: empty list """ species = [] warn("Unsubclassed update_species called for " + repr(self)) @@ -456,25 +716,52 @@ def update_species(self) -> List[Species]: # TODO implement abstractmethod def update_reactions(self) -> List[Species]: - """The subclasses should implement this method! + """Generate and return reactions associated with this component. + + Returns + ------- + list of Reaction + List of reaction objects generated by this component. This base + implementation returns an empty list. + + Notes + ----- + This method should be overridden by subclasses to return the actual + reactions generated by the component during CRN compilation. - :return: empty list """ reactions = [] warn("Unsubclassed update_reactions called for " + repr(self)) return reactions def enumerate_components(self, previously_enumerated=None) -> List: - """Enumerate components created by this component. - - This method is used for component enumeration. Usually you - will return a list of components that are copies of existing - ones (first list) and new components (second list). For - example, A DNA_construct makes a list of copies of its parts - as the first output, and a list of RNA_constructs as the - second output. An RNA_construct will make a list of copies of - its parts as the first output, and a list of Protein - components as its second output (if it makes any proteins) + """Enumerate derived components created from this component. + + This method generates new components based on the current component, + typically used during CRN compilation to expand higher-level + components into their constituent parts and products. + + Parameters + ---------- + previously_enumerated : set or list, optional + Collection of components that have already been enumerated, used + to prevent infinite recursion in component enumeration. + + Returns + ------- + list + List of new components created from this component. This base + implementation returns an empty list. + + Notes + ----- + Subclasses override this method to implement specific enumeration + behavior. For example: + + - A `DNA_construct` returns copies of its parts and `RNA_construct` + objects representing transcripts. + - An `RNA_construct` returns copies of its parts and `Protein` + components representing translation products. """ return [] diff --git a/biocrnpyler/core/mechanism.py b/biocrnpyler/core/mechanism.py index b805a4ba..7b2792c9 100644 --- a/biocrnpyler/core/mechanism.py +++ b/biocrnpyler/core/mechanism.py @@ -6,22 +6,69 @@ class Mechanism(object): - """Mechanism class for core mechanisms. - - Core mechanisms within a mixture (transcription, translation, etc) - - The Mechanism class is used to implement different core - mechanisms in TX-TL. All specific core mechanisms should be - derived from this class. + """Base class for mechanisms that generate species and reactions. + + Mechanisms are reaction schemas that define how components interact and + transform during CRN compilation. They represent molecular processes such + as transcription, translation, binding, catalysis, and degradation. Each + mechanism is called by components during compilation to generate the + appropriate species and reactions. + + Parameters + ---------- + name : str + Name of the mechanism instance for identification and debugging. + mechanism_type : str, default='' + Type identifier for the mechanism (e.g., 'transcription', + 'translation', 'binding'). Used for mechanism lookup in components + and mixtures. + + Attributes + ---------- + name : str + Name of the mechanism. + mechanism_type : str + Type identifier for the mechanism. + + See Also + -------- + Component : Base class that calls mechanisms during compilation. + Mixture : Container that provides default mechanisms to components. + + Notes + ----- + Subclasses must override `update_species` and `update_reactions` to + implement specific mechanism behavior. The base class implementations + return empty lists and issue warnings. + + If `mechanism_type` is empty or None, a warning is issued as this may + prevent proper mechanism inheritance and lookup. + + Examples + -------- + Create a custom mechanism by subclassing: + + >>> class CustomTranscription(bcp.Mechanism): + ... def __init__(self, name="custom_tx"): + ... super().__init__(name=name, mechanism_type="transcription") + ... + ... def update_species(self, dna, transcript, **kwargs): + ... # Generate RNA species + ... return [transcript] + ... + ... def update_reactions(self, dna, transcript, **kwargs): + ... # Generate transcription reaction + ... return [Reaction([dna], [dna, transcript], k=0.01)] + + Use a mechanism in a mixture: + + >>> mixture = bcp.Mixture( + ... mechanisms={'transcription': CustomTranscription()} + ... ) """ def __init__(self, name: str, mechanism_type=''): - """Initializes a Mechanism instance. - - :param name: name of the Mechanism - :param mechanism_type: mechanism_type in string - """ self.name = name self.mechanism_type = mechanism_type if mechanism_type == '' or mechanism_type is None: @@ -31,17 +78,67 @@ def __init__(self, name: str, mechanism_type=''): ) def update_species(self, component=None, part_id=None) -> List: - """The child class should implement this method. + """Generate species for this mechanism. + + Parameters + ---------- + component : Component, optional + The component calling this mechanism. May be used to access + component-specific parameters or attributes. + part_id : str, optional + Part identifier for parameter lookup. Used to retrieve + part-specific parameters from the parameter database. + + Returns + ------- + list of Species + List of species objects generated by this mechanism. This base + implementation returns an empty list. + + Warns + ----- + UserWarning + Issues a warning when the base class method is called, indicating + that subclasses should override this method. + + Notes + ----- + Subclasses must override this method to implement mechanism-specific + species generation logic. - :return: empty list """ warn(f"Default update_species called for mechanism {self.name}") return [] def update_reactions(self, component=None, part_id=None) -> List: - """The child class should implement this method. + """Generate reactions for this mechanism. + + Parameters + ---------- + component : Component, optional + The component calling this mechanism. May be used to access + component-specific parameters or attributes. + part_id : str, optional + Part identifier for parameter lookup. Used to retrieve + part-specific parameters from the parameter database. + + Returns + ------- + list of Reaction + List of reaction objects generated by this mechanism. This base + implementation returns an empty list. + + Warns + ----- + UserWarning + Issues a warning when the base class method is called, indicating + that subclasses should override this method. + + Notes + ----- + Subclasses must override this method to implement mechanism-specific + reaction generation logic. - :return: empty list """ warn(f"Default update_reactions called for mechanism {self.name}") return [] @@ -51,23 +148,94 @@ def __repr__(self): class EmptyMechanism(Mechanism): - """Mechanism that does nothing. - - For use when one needs a Mechanism to do nothing, such as - translation in Expression Mixtures. + """Mechanism that generates no species or reactions. + + A placeholder mechanism used when a mechanism type is required but no + actual reactions should be generated. Commonly used in Expression Mixtures + where translation is disabled, or to temporarily disable specific + mechanisms without removing them from the code. + + Parameters + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type identifier for the mechanism (e.g., 'translation'). + + See Also + -------- + Mechanism : Base class for all mechanisms. + + Notes + ----- + Both `update_species` and `update_reactions` return empty lists, + ensuring no CRN elements are generated when this mechanism is called. + + Examples + -------- + Use EmptyMechanism to disable translation in a mixture: + + >>> rnap = bcp.Species('RNAP') + >>> mixture = bcp.Mixture( + ... mechanisms={ + ... 'transcription': bcp.Transcription_MM(rnap), + ... 'translation': bcp.EmptyMechanism( + ... 'no_translation', 'translation') + ... } + ... ) + + Create a DNA assembly that transcribes but doesn't translate: + + >>> promoter = bcp.Promoter('pconst') + >>> assembly = bcp.DNAassembly( + ... name='tx_only', + ... promoter=promoter, + ... mechanisms={ + ... 'translation': bcp.EmptyMechanism('empty', 'translation') + ... } + ... ) """ def __init__(self, name, mechanism_type): - """Initializes an EmptyMechanism instance. - - :param name: name of the Mechanism - :param mechanism_type: mechanism_type in string - """ Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) def update_species(self, component=None, part_id=None, **kwargs): + """Generate empty list of species. + + Parameters + ---------- + component : Component, optional + The component calling this mechanism (unused). + part_id : str, optional + Part identifier (unused). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + Empty list. + + """ return [] def update_reactions(self, component=None, part_id=None, **kwargs): + """Generate empty list of reactions. + + Parameters + ---------- + component : Component, optional + The component calling this mechanism (unused). + part_id : str, optional + Part identifier (unused). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + Empty list. + + """ return [] diff --git a/biocrnpyler/core/mixture.py b/biocrnpyler/core/mixture.py index 3450b78e..598592f0 100644 --- a/biocrnpyler/core/mixture.py +++ b/biocrnpyler/core/mixture.py @@ -16,6 +16,131 @@ class Mixture(object): + """Container for components, mechanisms, and parameters in a CRN model. + + A Mixture holds together all components (DNA, RNA, Protein, etc.), + mechanisms (transcription, translation, binding, etc.), and parameters + needed to compile a chemical reaction network (CRN). Mixtures can include + default mechanisms that apply to all components, as well as global + mechanisms that affect all species (e.g., dilution, degradation). + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + Component : Base class for biomolecular components. + Mechanism : Base class for reaction generation schemas. + GlobalMechanism : Mechanisms that apply to all species. + ChemicalReactionNetwork : Container for species and reactions. + + Notes + ----- + Components added to a Mixture are deep-copied to prevent unintended + modifications. Each component's `mixture` attribute is set to reference + this Mixture, allowing components to access default mechanisms and + parameters. + + The compilation process follows these steps: + + 1. Global component enumeration (e.g., generating all interactions) + 2. Local component enumeration (e.g., DNA --> RNA --> Protein) + 3. Species generation from all enumerated components + 4. Reaction generation from all enumerated components + 5. Application of global mechanisms to all species + + Examples + -------- + Create a basic mixture with components and mechanisms: + + >>> # Create a simple transcription-translation mixture + >>> mixture = bcp.Mixture( + ... name="txtl_extract", + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': bcp.SimpleTranslation() + ... }, + ... parameters={'ktx': 0.05, 'ktl': 0.01} + ... ) + + Add components to the mixture: + + >>> promoter = bcp.Promoter("pconst") + >>> gene = bcp.DNA_construct([promoter, bcp.RBS('rbs'), bcp.CDS('gfp')]) + >>> mixture.add_component(gene) + + Compile the CRN: + + >>> crn = mixture.compile_crn() + >>> print( + ... f"CRN has {len(crn.species)} species " + ... f"and {len(crn.reactions)} reactions") + + """ + def __init__( self, name='', @@ -32,27 +157,6 @@ def __init__( global_recursion_depth=4, local_recursion_depth=None, ): - """Mixture object holding components, mechanisms, and parameters. - - A Mixture object holds together all the components - (DNA,Protein, etc), mechanisms (Transcription, Translation), - and parameters related to the mixture itself - (e.g. Transcription rate). Default components and mechanisms - can be added as well as global mechanisms that impacts all - species (e.g. cell growth). - - :param name: Name of the mixture - :param mechanisms: Dictionary of mechanisms - :param components: List of components in the mixture (list of - Components) - :param parameters: Dictionary of parameters (check parameters - documentation for the keys) - :param parameter_file: Parameters can be loaded from a parameter file - :param default_mechanisms: - :param global_mechanisms: dict of global mechanisms that impacts - all species (e.g. cell growth) - - """ # Initialize instance variables self.name = name # Save the name of the mixture self.compartment = compartment @@ -73,7 +177,7 @@ def __init__( if mechanisms is None and not hasattr(self, '_mechanisms'): self.mechanisms = {} else: - self.add_mechanisms(mechanisms) + self.add_mechanisms(mechanisms, overwrite=True) # process global_mechanisms: # Global mechanisms are applied just once ALL species generated from @@ -107,6 +211,25 @@ def __init__( self.crn = None def add_species(self, species: Union[List[Species], Species]): + """Add species directly to the mixture without component compilation. + + Parameters + ---------- + species : Species or list of Species + Species object(s) to add directly to the mixture. These species + will be included in the CRN during compilation. + + Raises + ------ + AssertionError + If any element in the list is not a Species object. + + Notes + ----- + Species added this way bypass component enumeration and are added + directly to the CRN during `compile_crn`. + + """ if not hasattr(self, 'added_species'): self.added_species = [] @@ -128,12 +251,31 @@ def set_species( material_type=None, attributes=None, ): - """Used to set internal species from strings, Species or Components. + """Convert various inputs into Species objects. + + Parameters + ---------- + species : Species, str, or Component + The species to convert. Can be a `Species` object (returned + as-is), a string (creates new Species), or a `Component` (extracts + its species). + material_type : str, optional + Material type for the species (e.g., 'dna', 'rna', 'protein'). + Only used when creating new Species from strings. + attributes : list of str, optional + Attributes to assign to the species. Only used when creating + new Species from strings. + + Returns + ------- + Species + The converted Species object. + + Raises + ------ + ValueError + If the input cannot be converted to a valid Species. - :param species: name of a species or a species instance - :param material_type: material type of a species as a string - :param attributes: Species attribute - :return: Species in the mixture """ if isinstance(species, Species): return species @@ -165,7 +307,31 @@ def components(self, components): self.add_components(components) def add_component(self, component): - """This function adds a single component to the mixture.""" + """Add a single component to the mixture. + + Parameters + ---------- + component : Component or list of Component + Component object to add to the mixture. If a list is provided, + calls `add_components` instead. The component is deep-copied + before being added. + + Raises + ------ + AssertionError + If the component is not a Component object. + ValueError + If a component with the same type and name already exists in + the mixture. + + Notes + ----- + Components are deep-copied when added to prevent modification of the + original component. The copied component's `mixture` attribute is set + to this Mixture, and its `compartment` is set to the mixture's + compartment. + + """ if not hasattr(self, '_components'): self.components = [] @@ -194,9 +360,24 @@ def add_component(self, component): self.components.append(component_copy) def get_mechanism(self, mechanism_type): - """Searches the Mixture for a Mechanism of the correct type. + """Retrieve a mechanism by type from the mixture. + + Parameters + ---------- + mechanism_type : str + The type identifier of the mechanism to retrieve (e.g., + 'transcription', 'translation', 'binding'). + + Returns + ------- + Mechanism or None + The requested mechanism object, or None if not found. + + Raises + ------ + TypeError + If `mechanism_type` is not a string. - If no Mechanism is found, None is returned. """ if not isinstance(mechanism_type, str): raise TypeError( @@ -209,7 +390,25 @@ def get_mechanism(self, mechanism_type): return None def add_components(self, components: Union[List[Component], Component]): - """This function adds a list of components to the mixture.""" + """Add multiple components to the mixture. + + Parameters + ---------- + components : Component or list of Component + Component object(s) to add to the mixture. Each component is + deep-copied before being added. + + Raises + ------ + ValueError + If `components` is not a Component, list of Components, or if + any duplicate components are detected. + + See Also + -------- + add_component : Add a single component to the mixture. + + """ if isinstance(components, Component): self.add_component(components) elif isinstance(components, List): @@ -222,16 +421,34 @@ def add_components(self, components: Union[List[Component], Component]): ) def get_component(self, component=None, name=None, index=None): - """Function to get components from Mixture._components. - - One of the 3 keywords must not be None. + """Retrieve components from the mixture by various criteria. + + Exactly one of the three parameters must be provided. + + Parameters + ---------- + component : Component, optional + A component instance to search for. Returns components with + matching type and name. + name : str, optional + Name of the component to search for. Returns all components + with this name. + index : int, optional + Index of the component in the mixture's component list. + + Returns + ------- + Component, list of Component, or None + - Single Component if exactly one match is found or index is used + - List of Components if multiple matches are found + - None if no matches are found + + Raises + ------ + ValueError + If zero or more than one parameter is provided, or if parameters + are of incorrect types. - :param component: an instance of a component. Searches - Mixture._components for a Component with the same type and name. - :param name: str. Searches Mixture._components for a Component - with the same name - :param index: int. returns Mixture._components[index] - :return: if nothing is found, returns None. """ if [component, name, index].count(None) != 2: raise ValueError( @@ -274,7 +491,7 @@ def get_component(self, component=None, name=None, index=None): @property def mechanisms(self): - """Mechanisms stores Mixture Mechanisms.""" + """Mechanism: Stores mixture mechanisms.""" return self._mechanisms @mechanisms.setter @@ -283,17 +500,32 @@ def mechanisms(self, mechanisms): self.add_mechanisms(mechanisms, overwrite=True) def add_mechanism(self, mechanism, mech_type=None, overwrite=False): - """Add mechanism to mixture dictionary. - - Adds a mechanism of type mech_type to the Mixture - mechanism_dictionary. - - :param mechanism: a Mechanism instance - :param mech_type: the type of mechanism. defaults to - mechanism.mech_type if None - :param overwrite: whether to overwrite existing mechanisms of - the same type (default False) - :return: + """Add a mechanism to the mixture's mechanism dictionary. + + Parameters + ---------- + mechanism : Mechanism or GlobalMechanism + The mechanism object to add. If a `GlobalMechanism` is provided, + it is automatically added to the global mechanisms dictionary. + mech_type : str, optional + The type key under which to store the mechanism. If None, uses + the mechanism's `mechanism_type` attribute. + overwrite : bool, default=False + If True, replaces any existing mechanisms with the same keys. If + False, raises ValueError when keys already exist. If None, ignores + mechanisms that already exist. + + Raises + ------ + TypeError + If `mechanism` is not a Mechanism object, or if `mech_type` is + not a string. + ValueError + If mechanism key already exists and `overwrite` is None. + + See Also + -------- + add_global_mechanism : Add a global mechanism specifically. """ if not hasattr(self, '_mechanisms'): @@ -315,23 +547,41 @@ def add_mechanism(self, mechanism, mech_type=None, overwrite=False): self.add_global_mechanism(mechanism, mech_type, overwrite) elif isinstance(mechanism, Mechanism): if mech_type in self._mechanisms and not overwrite: - raise ValueError( - f"mech_type {mech_type} already in Mixture {self}. " - f"To overwrite, use keyword overwrite = True." - ) + if overwrite is False: + raise ValueError( + f"mech_type {mech_type} already in Mixture {self}. " + f"To overwrite, use keyword overwrite=True." + ) else: self._mechanisms[mech_type] = copy.deepcopy(mechanism) def add_mechanisms(self, mechanisms, overwrite=False): - """Add mechanism to mixture dictionary. - - This function adds a list or dictionary of mechanisms to the - mixture. Can take both GlobalMechanisms and Mechanisms. - - :param mechanisms: a Mechanism instance - :param overwrite: whether to overwrite existing mechanisms of - the same type (default False) - :return: + """Add multiple mechanisms to the mixture. + + Accepts mechanisms as a single object, list, or dictionary and adds + them to the mixture's mechanism dictionary. Can handle both regular + `Mechanism` and `GlobalMechanism` objects. + + Parameters + ---------- + mechanisms : Mechanism, GlobalMechanism, dict, or list + The mechanism(s) to add. Can be a single mechanism, a dict with + mechanism types as keys and mechanisms as values, or a list of + mechanisms. + overwrite : bool, default=False + If True, replaces any existing mechanisms with the same keys. If + False, raises ValueError when keys already exist. If None, ignores + mechanisms that already exist. + + Raises + ------ + ValueError + If `mechanisms` is not a valid type, or if mechanism key conflicts + occur with `overwrite=False`. + + See Also + -------- + add_mechanism : Add a single mechanism to the mixture. """ if isinstance(mechanisms, Mechanism): @@ -352,7 +602,7 @@ def add_mechanisms(self, mechanisms, overwrite=False): @property def global_mechanisms(self): - """Stores global Mechanisms in the Mixture.""" + """Mechanism: Stores global mechanisms in the mixture.""" return self._global_mechanisms @global_mechanisms.setter @@ -372,15 +622,33 @@ def add_global_mechanism( ): """Add a global mechanism to the mixture. - Adds a mechanism of type mech_type to the Mixture - global_mechanism dictonary. - - keywords: - mechanism: a Mechanism instance - mech_type: the type of mechanism. defaults to mechanism.mech_type - if None - overwrite: whether to overwrite existing mechanisms of the same \ - type (default False) + Global mechanisms are applied to all species after component + compilation, enabling operations like dilution or global degradation. + + Parameters + ---------- + mechanism : GlobalMechanism + The global mechanism object to add. Must be a `GlobalMechanism` + instance. + mech_type : str, optional + The type key under which to store the mechanism. If None, uses + the mechanism's `mechanism_type` attribute. + overwrite : bool, default=False + If True, replaces any existing global mechanism with the same key. + If False, raises ValueError when key already exists. + + Raises + ------ + TypeError + If `mechanism` is not a GlobalMechanism object, or if `mech_type` + is not a string. + ValueError + If mechanism key already exists and `overwrite` is False. + + Notes + ----- + Global mechanisms are applied during `compile_crn` after all + component reactions have been generated. """ if not hasattr(self, '_global_mechanisms'): @@ -409,6 +677,20 @@ def add_global_mechanism( def update_parameters( self, parameter_file=None, parameters=None, overwrite_parameters=True ): + """Update the parameter database with new parameters. + + Parameters + ---------- + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + parameters : dict, optional + Dictionary of parameters to add. Keys follow the format + (mechanism, part_id, param_name). + overwrite_parameters : bool, default=True + If True, new parameter values overwrite existing ones. If False, + existing parameters are preserved. + + """ if parameter_file is not None: self.parameter_database.load_parameters_from_file( parameter_file, overwrite_parameters=overwrite_parameters @@ -420,6 +702,23 @@ def update_parameters( ) def get_parameter(self, mechanism, part_id, param_name): + """Retrieve a parameter from the mixture's parameter database. + + Parameters + ---------- + mechanism : str + Mechanism identifier for the parameter lookup key. + part_id : str + Part identifier for the parameter lookup key. + param_name : str + Name of the parameter to retrieve. + + Returns + ------- + Parameter or None + The parameter object, or None if not found. + + """ param = self.parameter_database.find_parameter( mechanism, part_id, param_name ) @@ -428,28 +727,43 @@ def get_parameter(self, mechanism, part_id, param_name): def get_initial_concentration( self, S: Union[List, Species], component=None ): - """Get initial concentration. + """Determine initial concentrations using parameter hierarchy. + + Searches for initial concentration parameters for species following a + hierarchical lookup strategy, defaulting to 0 if not found. + + Parameters + ---------- + S : Species or list of Species + Species object(s) for which to find initial concentrations. Lists + are automatically flattened. + component : Component, optional + The component that generated the species. Used for + component-specific parameter lookup. - Tries to find an initial condition of species s using the - parameter hierarchy using the key: + Returns + ------- + dict + Dictionary mapping each Species object to its initial + concentration value (float). - 1. Searches Component's ParameterDatabase using the key: - mechanisms = "initial concentration" - part_id = mixture.name - parameter_name = str(s) + Raises + ------ + ValueError + If any element in `S` is not a Species object. - if s == component.get_species, also checks with - parameter_name=component.name + Notes + ----- + The parameter lookup hierarchy is: - 2. Searches the Mixture's ParameterDatabase using the key: - mechanisms = "initial concentration" - part_id = mixture.name - parameter_name = str(s) + 1. Component's `ParameterDatabase` with `mechanism='initial + concentration'`, `part_id=mixture.name`, and parameter names: + `str(s)`, `s.name`, or `component.name` (where `s` is the + component's primary species) - if s == component.get_species, also checks with - parameter_name=component.name + 2. Mixture's `ParameterDatabase` with the same keys - 3. Defaults to 0 + 3. Defaults to 0 if not found """ if isinstance(S, Species): @@ -531,6 +845,39 @@ def add_species_to_crn( copy_species=True, compartment=None, ): + """Add species to the CRN with initial concentrations. + + Helper method that adds species to the CRN and automatically looks up + and assigns their initial concentrations. + + Parameters + ---------- + new_species : Species or list of Species + Species to add to the CRN. + component : Component, optional + The component that generated these species. Used for + component-specific initial concentration lookup. + no_initial_concentrations : bool, default=False + If True, skips initial concentration lookup and assignment. + copy_species : bool, default=True + If True, deep-copies species before adding them to the CRN. + compartment : Compartment, optional + Compartment to assign to the species. Overrides species' + existing compartments. + + Returns + ------- + list of Species + All species in the CRN after addition (may include pre-existing + species). + + Notes + ----- + This method tracks which species are newly added and only assigns + initial concentrations to those new species, preventing overwriting + of previously set initial concentrations. + + """ if self.crn is None: self.crn = ChemicalReactionNetwork(species=[], reactions=[]) @@ -572,8 +919,31 @@ def add_species_to_crn( def apply_global_mechanisms( self, species, compartment=None ) -> Tuple[List[Species], List[Reaction]]: - # update with global mechanisms + """Apply all global mechanisms to a set of species. + + Calls each global mechanism's `update_species_global` and + `update_reactions_global` methods, then adds the resulting species + and reactions to the CRN. + + Parameters + ---------- + species : list of Species + Species to which global mechanisms should be applied. + compartment : Compartment, optional + Compartment for newly generated species and reactions. + + Returns + ------- + tuple of (list of Species, list of Reaction) + New species and reactions generated by global mechanisms. + + Notes + ----- + Global mechanisms are typically used for operations that affect all + species uniformly, such as dilution, global degradation, or + compartment transport. + """ global_mech_species = [] global_mech_reactions = [] if self.global_mechanisms: @@ -595,8 +965,34 @@ def apply_global_mechanisms( def component_enumeration( self, comps_to_enumerate=None, recursion_depth=10 ) -> List[Component]: - # Components that produce components through Component Enumeration + """Recursively enumerate components to generate derived components. + + Calls each component's `enumerate_components` method repeatedly to + expand high-level components into their constituent parts (e.g., + DNA_construct --> RNA_construct --> Protein). + + Parameters + ---------- + comps_to_enumerate : list of Component, optional + Initial components to enumerate. If None, uses all components in + the mixture. + recursion_depth : int, default=10 + Maximum number of enumeration iterations. Prevents infinite + recursion. + + Returns + ------- + list of Component + All components including the original components and all derived + components generated through enumeration. + + Warns + ----- + UserWarning + Warns if unenumerated components remain after reaching the + recursion depth limit. + """ all_components = [] new_components = [] if comps_to_enumerate is None: @@ -629,7 +1025,34 @@ def component_enumeration( def global_component_enumeration( self, comps_to_enumerate=None, recursion_depth=None ) -> List[Component]: - """Components that produce other components infinitely.""" + """Apply global component enumerators to generate new components. + + Global component enumerators create new components based on patterns + across all components (e.g., generating all pairwise binding + interactions between proteins). + + Parameters + ---------- + comps_to_enumerate : list of Component, optional + Initial components to pass to enumerators. If None, uses all + components in the mixture. + recursion_depth : int, optional + Maximum number of enumeration iterations. If None, uses + `self.global_recursion_depth`. + + Returns + ------- + list of Component + All components including original and newly generated components + from global enumeration. + + Notes + ----- + This method is called during `compile_crn` before local component + enumeration. Global enumerators are useful for creating complex + interaction networks without manually specifying every interaction. + + """ if recursion_depth is None: recursion_depth = self.global_recursion_depth @@ -683,24 +1106,87 @@ def compile_crn( add_reaction_species: bool = True, compartment: Compartment = None, ) -> ChemicalReactionNetwork: - """Create chemical reaction network from mixture species, reactions. - - :param initial_concentration_dict: a dictionary to overwrite initial - concentrations at the end of compile time - :param recursion_depth: how deep to run the Local and - Global Component Enumeration - :param return_enumerated_components: returns a list of all enumerated - components along with the CRN - :param initial_concentrations_at_end: if True does not look in - Components for Species' initial concentrations and only checks - the Mixture database at the end. - :param copy_objects: Species and Reactions will be copied when placed - into the CRN. Protects CRN validity at the expense of compilation + """Compile a chemical reaction network from the mixture. + + Enumerates components, generates species and reactions from each + component, applies global mechanisms, and returns a complete CRN. + + Parameters + ---------- + recursion_depth : int, optional + Maximum recursion depth for both local and global component + enumeration. If None, uses `self.global_recursion_depth`. + initial_concentration_dict : dict, optional + Dictionary mapping species to initial concentrations. This + overrides all other initial concentration settings and is applied + at the very end of compilation. + return_enumerated_components : bool, default=False + If True, returns a tuple of (CRN, enumerated_components) instead + of just the CRN. + initial_concentrations_at_end : bool, default=False + If True, initial concentrations are only set at the end using the + mixture's parameter database, ignoring component-specific initial + concentrations during compilation. + copy_objects : bool, default=True + If True, species and reactions are deep-copied when added to the + CRN. Protects CRN validity at the expense of compilation speed. + add_reaction_species : bool, default=True + If True, species appearing in reactions are automatically added to + the CRN. Ensures no missing species at the expense of compilation speed. - :param add_reaction_species: Species inside reactions will be - added to the CRN. Ensures no missing species at the expense - of compilation speed. - :return: ChemicalReactionNetwork + compartment : Compartment, optional + Compartment to assign to all species and reactions that are not + already assigned to a compartment. If None, uses + `self.compartment`. + + Returns + ------- + ChemicalReactionNetwork or tuple + If `return_enumerated_components` is False, returns the compiled + `ChemicalReactionNetwork`. If True, returns a tuple of + (ChemicalReactionNetwork, list of enumerated Components). + + Notes + ----- + The compilation process follows these steps: + + 1. Add any directly-added species to the CRN + 2. Global component enumeration (generates component interactions) + 3. Local component enumeration (e.g., DNA --> RNA --> Protein) + 4. Generate species from all enumerated components + 5. Generate reactions from all enumerated components + 6. Apply global mechanisms to all species + 7. Set initial concentrations + + Examples + -------- + Basic compilation: + + >>> gene = bcp.DNAassembly( + ... 'GFP', promoter='pconst', rbs='RBS', protein='GFP') + >>> mixture = bcp.Mixture( + ... name="txtl_extract", + ... components=[gene], + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': bcp.SimpleTranslation() + ... }, + ... parameters={'ktx': 0.05, 'ktl': 0.01} + ... ) + >>> crn = mixture.compile_crn() + + Compilation with custom initial concentrations: + + >>> crn = mixture.compile_crn( + ... initial_concentration_dict={gene.dna: 1, gene.transcript: 50} + ... ) + + Get both CRN and enumerated components: + + >>> crn, components = mixture.compile_crn( + ... return_enumerated_components=True + ... ) + """ resetwarnings() @@ -710,7 +1196,7 @@ def compile_crn( self.crn = ChemicalReactionNetwork([], []) # helper to flatten & drop duplicates via ==, retagging - # default→compartment + # default -> compartment def _filter_new_by_eq(cands, existing_list): # flatten nested lists flat = [] diff --git a/biocrnpyler/core/parameter.py b/biocrnpyler/core/parameter.py index 407981c4..88363897 100644 --- a/biocrnpyler/core/parameter.py +++ b/biocrnpyler/core/parameter.py @@ -10,38 +10,39 @@ """Parameter processing module. -#### Parameter Value Defaulting - -Not all parameters need to have the required headings. The only two -required columns are "param_val" and "param_name". BioCRNpyler uses a form -of parameter name defaulting discussed below to find default parameters if -no exact match is in the config file. This makes it easy to set default -parameters for things like "ku" and "ktx" to quickly build models. - -#### Parameters inside BioCRNpyler: - -Inside of bioCRNpyler, parameters are stored as a dictionary key value -pair: (mechanism_name, part_id, param_name) --> param_val. If that -particular parameter key cannot be found, the software will default to the -following keys: (mechanism_type, part_id, param_name) >> (part_id, -param_name) >> (mechanism_name, param_name) >> (mechanism_type, param_name) ->> (param_name) and give a warning. As a note, mechanism_name refers to -the .name variable of a Mechanism. mechanism_type refers to the .type -variable of a Mechanism. Either of these can be used as a -mechanism_id. This allows for models to be constructed easily using default -parameter values and for parameters to be shared between different -Mechanisms and/or Components. - -#### Initial Conditions are also Parameters - -The initial condition of any Species (or Component) will also be looked up +*Parameter Value Defaulting* + +Not all parameters need to have the required headings. The only two required +columns are 'param_val' and 'param_name'. BioCRNpyler uses a form of +parameter name defaulting discussed below to find default parameters if no +exact match is in the config file. This makes it easy to set default +parameters for things like 'ku' and 'ktx' to quickly build models. + +*Parameters inside BioCRNpyler* + +Inside of bioCRNpyler, parameters are stored as a dictionary key value pair: +`(mechanism_name, part_id, param_name) --> param_val`. If that particular +parameter key cannot be found, the software will default to the following +keys: `(mechanism_type, part_id, param_name)` >> `(part_id, param_name)` >> +`(mechanism_name, param_name)` >> `(mechanism_type, param_name)` >> +`(param_name)` and give a warning. As a note, `mechanism_name` refers to the +`.name` variable of a `Mechanism`, and `mechanism_type` refers to the `.type` +variable of a `Mechanism`. Either of these can be used as a +`mechanism_id`. This allows for models to be constructed easily using default +parameter values and for parameters to be shared between different mechanisms +and/or components. + +Units are read directly read from the column labeled "units" in the +parameter file. + +*Initial Conditions are also Parameters* + +The initial condition of any `Species` (or `Component`) will also be looked up as a parameters automatically. Initial conditions can be customized in -through the custom_initial_conditions keyword in the Mixture constructor. -custom_initial_conditions will take precedent to parameter initial -conditions. +through the `custom_initial_conditions` keyword in the `Mixture` constructor. +The `custom_initial_conditions` keyword will take precedent over parameter +initial conditions. -#### Units are read directly read from the column labeled "units" in the -#### parameter file. """ import csv @@ -53,22 +54,103 @@ # This could later be extended ParameterKey = namedtuple('ParameterKey', 'mechanism part_id name') +"""Named tuple defining a parameter key. + + Parameters + ---------- + mechanism : str, Mechanism, or None + Mechanism identifier. Can be a string (used as both name and type), a + Mechanism object (uses .name and .mechanism_type), or None. + part_id : str or None + Part/component identifier for the parameter. + name : str + Name of the parameter. Must start with a letter and contain at + least one character. + +""" class Parameter(object): + """Base class for representing parameters in BioCRNpyler. + + Parameters represent kinetic constants, initial concentrations, and + other numerical values used in chemical reaction networks. This class + provides validation for parameter names, values, and units. + + Parameters + ---------- + parameter_name : str + Name of the parameter. Must start with a letter and contain at + least one character. + parameter_value : float or str + Value of the parameter. Can be a number or a string in formats: + '1.00', '1e4', or '2/5' (rational). Strings are automatically + converted to numerical values. + unit : str, optional + Unit of the parameter (e.g., '1/s', 'nM', 'molecules'). If None, + defaults to empty string. + + Attributes + ---------- + parameter_name : str + The validated name of the parameter. + value : float + The numerical value of the parameter. + unit : str + The unit string associated with the parameter. + + See Also + -------- + ParameterEntry : Parameter with database lookup keys. + ModelParameter : Parameter with search and found keys for defaulting. + ParameterDatabase : Database for storing and retrieving parameters. + + Notes + ----- + This is the base class for all parameter types in BioCRNpyler. In + practice, subclasses `ParameterEntry` and `ModelParameter` are more + commonly used as they support the parameter lookup and defaulting + system. + + Parameter values provided as strings are automatically converted: + + - '1e4' --> 10000.0 + - '2/5' --> 0.4 + - '1.23' --> 1.23 + + Examples + -------- + Create a basic parameter: + + >>> param = bcp.Parameter('kb', 100.0, unit='1/s') + >>> param.parameter_name + 'kb' + >>> param.value + 100.0 + + Create a parameter from a string value: + + >>> param = bcp.Parameter('ku', '1e-4', unit='1/s') + >>> param.value + 0.0001 + + Create a parameter from a rational string: + + >>> param = bcp.Parameter('ratio', '3/4') + >>> param.value + 0.75 + + """ + def __init__( self, parameter_name: str, parameter_value: Union[str, numbers.Real], unit=None, ): - """A class for representing parameters in general. - - Only the below subclasses are ever used. + """Initialize a Parameter object. - :param parameter_name: is the name of the parameter - :param parameter_value: is the value of the parameter - :param unit: is the unit of the parameter or a species + See class docstring for parameter descriptions. """ self.parameter_name = parameter_name @@ -77,10 +159,26 @@ def __init__( @property def parameter_name(self) -> str: + """str: The name of the parameter.""" return self._parameter_name @parameter_name.setter def parameter_name(self, new_parameter_name: str): + """Set the parameter name with validation. + + Parameters + ---------- + new_parameter_name : str + New name for the parameter. Must be a string starting with a + letter (not a number). + + Raises + ------ + ValueError + If `new_parameter_name` is not a string, or if it doesn't start + with a letter. + + """ if not isinstance(new_parameter_name, str): raise ValueError( f"parameter_name must be a string: " @@ -96,10 +194,26 @@ def parameter_name(self, new_parameter_name: str): @property def value(self) -> numbers.Real: + """float: The numerical value of the parameter.""" return self._value @value.setter def value(self, new_parameter_value: Union[str, numbers.Real]): + """Set the parameter value with validation and conversion. + + Parameters + ---------- + new_parameter_value : float or str + New value for the parameter. If a string, must be in format + '1.00', '1e4', or '2/5'. Strings are automatically converted + to float. + + Raises + ------ + ValueError + If `new_parameter_value` is not a number or valid string format. + + """ if not ( isinstance(new_parameter_value, numbers.Real) or isinstance(new_parameter_value, str) @@ -129,10 +243,24 @@ def value(self, new_parameter_value: Union[str, numbers.Real]): @property def unit(self) -> str: + """str: The unit string for the parameter.""" return self._unit @unit.setter def unit(self, new_unit: str): + """Set the parameter unit. + + Parameters + ---------- + new_unit : str or None + Unit string for the parameter. If None, sets to empty string. + + Raises + ------ + ValueError + If `new_unit` is not a string or None. + + """ if new_unit is None: self._unit = '' elif not isinstance(new_unit, str): @@ -144,6 +272,23 @@ def unit(self, new_unit: str): @staticmethod def _convert_rational(p_value: str) -> numbers.Real: + """Convert a string parameter value to a numerical value. + + Handles rational fractions (e.g., '2/5') and standard float + strings (e.g., '1.23' or '1e4'). + + Parameters + ---------- + p_value : str + String representation of parameter value. Can be a fraction + ('2/5') or standard float format ('1.23', '1e4'). + + Returns + ------- + float + Numerical value of the parameter. + + """ if '/' in p_value: nom, denom = p_value.split('/') return float(nom) / float(denom) @@ -151,6 +296,25 @@ def _convert_rational(p_value: str) -> numbers.Real: return float(p_value) def __eq__(self, other): + """Test equality between parameters or parameter and number. + + Parameters + ---------- + other : Parameter or float + Object to compare with. Can be another Parameter object or a + numerical value. + + Returns + ------- + bool + True if values are equal, False otherwise. + + Raises + ------ + TypeError + If `other` cannot be compared (not a Parameter or number). + + """ if isinstance(other, Parameter): return self.value == other.value else: @@ -162,25 +326,100 @@ def __eq__(self, other): ) def __str__(self): + """Return string representation of the parameter. + + Returns + ------- + str + String in format 'Parameter = '. + + """ return f"Parameter {self.parameter_name} = {self.value}" def __hash__(self): + """Return hash value for the parameter. + + Returns + ------- + int + Hash value based on parameter name, value, and unit. + + """ return ( hash(self._parameter_name) + hash(self._value) + hash(self._unit) ) class ParameterEntry(Parameter): - """Parameter stored the ParameterDatabase. - - parameter_keys is a dictionary {key:value} or named_tuple (type - ParameterKey) of keys for looking up the parameter. - - parameter_info is a dictionary {key:value} of additional - information about the parameter. - - For example: additional columns in the parameter file or the - parameter file name. + """Parameter with database lookup key and metadata. + + A `ParameterEntry` extends `Parameter` with a lookup key for database + storage and retrieval, plus additional metadata about the parameter's + origin and context. + + Parameters + ---------- + parameter_name : str + Name of the parameter. + parameter_value : float or str + Value of the parameter. + parameter_key : dict, ParameterKey, str, or None, optional + Lookup key for the parameter database. + parameter_info : dict, optional + Additional metadata about the parameter (e.g., source file, + comments). If dict contains 'unit' key, it will update the + parameter's unit. + **kwargs + Additional keyword arguments passed to Parameter constructor, + including 'unit'. + + Attributes + ---------- + parameter_key : ParameterKey + The lookup key as a named tuple (mechanism, part_id, name). + parameter_info : dict + Dictionary of additional parameter metadata. + + See Also + -------- + Parameter : Base parameter class. + ModelParameter : Parameter with search and found keys. + ParameterDatabase : Database for storing parameter entries. + + Notes + ----- + The `parameter_key` value can be any of the following: + + - dict: {'mechanism': ..., 'part_id': ..., 'name': ...} + - ParameterKey namedtuple: (mechanism, part_id, name) + - str: parameter name (other fields set to None) + - None: creates key with all fields None except name + + using the following conventions: + + - mechanism: str or None (mechanism name or type) + - part_id: str or None (component/part identifier) + - name: str (parameter name) + + These keys enable flexible parameter lookup with defaulting behavior + in the ParameterDatabase. + + Examples + -------- + Create a parameter entry with full key: + + >>> entry = bcp.ParameterEntry( + ... 'kb', + ... 100.0, + ... parameter_key={'mechanism': 'binding', 'part_id': 'promoter1'}, + ... unit='1/s' + ... ) + + Create a parameter entry with just a name: + + >>> entry = bcp.ParameterEntry('ku', 0.01, parameter_key='ku') + >>> entry.parameter_key + ParameterKey(mechanism=None, part_id=None, name='ku') """ @@ -192,6 +431,11 @@ def __init__( parameter_info=None, **kwargs, ): + """Initialize a ParameterEntry object. + + See class docstring for parameter descriptions. + + """ Parameter.__init__(self, parameter_name, parameter_value, **kwargs) self.parameter_key = parameter_key @@ -202,6 +446,47 @@ def __init__( def create_parameter_key( new_key: Union[Dict, ParameterKey, str], parameter_name=None ) -> ParameterKey: + """Convert various input types to a ParameterKey namedtuple. + + Parameters + ---------- + new_key : dict, ParameterKey, tuple, str, or None + Input to convert to ParameterKey: + + - dict: Must have keys matching ParameterKey fields + - ParameterKey: Returned as-is + - 3-tuple: Converted to ParameterKey with proper field mapping + - str: Used as 'name', other fields set to None + - None: All fields set to None (requires parameter_name) + + parameter_name : str, optional + Parameter name to use if not specified in new_key. Overrides + `name` field if provided in dict. + + Returns + ------- + ParameterKey + Named tuple with fields (mechanism, part_id, name). + + Raises + ------ + ValueError + If `new_key` is not a valid type or format. + + Examples + -------- + >>> key = bcp.ParameterEntry.create_parameter_key('kb') + >>> key + ParameterKey(mechanism=None, part_id=None, name='kb') + + >>> key = bcp.ParameterEntry.create_parameter_key( + ... {'mechanism': 'transcription', 'part_id': 'prom1'}, + ... parameter_name='ktx' + ... ) + >>> key + ParameterKey(mechanism='transcription', part_id='prom1', name='ktx') + + """ # New Key can be a named_tuple if isinstance(new_key, dict): new_key = dict(new_key) @@ -240,20 +525,46 @@ def create_parameter_key( @property def parameter_key(self) -> ParameterKey: + """ParameterKey: The database lookup key for this parameter.""" return self._parameter_key @parameter_key.setter def parameter_key(self, parameter_key: Union[Dict, ParameterKey, str]): + """Set the parameter lookup key. + + Parameters + ---------- + parameter_key : dict, ParameterKey, str, or None + New parameter key. Automatically converted to ParameterKey + namedtuple using `create_parameter_key`. + + """ self._parameter_key = self.create_parameter_key( parameter_key, self.parameter_name ) @property def parameter_info(self) -> Dict: + """dict: Additional metadata about the parameter.""" return self._parameter_info @parameter_info.setter def parameter_info(self, parameter_info: Dict): + """Set parameter metadata. + + Parameters + ---------- + parameter_info : dict or None + Dictionary of additional parameter information. If dict + contains 'unit' key, updates the parameter's unit attribute. + + Raises + ------ + ValueError + If `parameter_info` is not None or a dict, or if 'unit' in + dict conflicts with existing unit. + + """ if parameter_info is None: self._parameter_info = {} elif isinstance(parameter_info, dict): @@ -280,6 +591,17 @@ def parameter_info(self, parameter_info: Dict): ) def get_sbml_id(self): + """Generate SBML-compatible identifier for the parameter. + + Constructs an identifier string from the parameter key fields, + formatted as: '__'. + + Returns + ------- + str + SBML-compatible identifier string. + + """ sbml_id = self.parameter_key.name + '_' if self.parameter_key.part_id is not None: sbml_id += self.parameter_key.part_id @@ -289,17 +611,86 @@ def get_sbml_id(self): return sbml_id def __str__(self): - return f"ParameterEntry({self.parameter_key}) = {self.value}" + """Return string representation of the parameter entry. + Returns + ------- + str + String in format 'ParameterEntry() = '. -class ModelParameter(ParameterEntry): - """A class for representing parameters used in the Model. + """ + return f"ParameterEntry({self.parameter_key}) = {self.value}" - search_key is a tuple searched for to find the parameter, eg - (mech_id, part_id, param_name), - found_key is the tuple used after defaulting to find the - parameter eg (param_name) +class ModelParameter(ParameterEntry): + """Parameter with search and found keys for defaulting behavior. + + A `ModelParameter` extends `ParameterEntry` with information about how + the parameter was looked up in the database. It tracks both the + original search key and the actual key where the parameter was found, + enabling parameter defaulting and debugging. + + Parameters + ---------- + parameter_name : str + Name of the parameter. + parameter_value : float or str + Value of the parameter. + search_key : dict, ParameterKey, tuple, or str + The key originally searched for in the database. Usually includes + mechanism, part_id, and name. + found_key : dict, ParameterKey, tuple, or str + The key where the parameter was actually found after defaulting. + May have fewer fields than search_key. + unit : str, optional + Unit of the parameter. + parameter_key : dict, ParameterKey, str, or None, optional + Database lookup key (inherited from ParameterEntry). + parameter_info : dict, optional + Additional metadata (inherited from ParameterEntry). + **kwargs + Additional keyword arguments passed to ParameterEntry constructor. + + Attributes + ---------- + search_key : ParameterKey + The original lookup key as a named tuple. + found_key : ParameterKey + The key where parameter was found as a named tuple. + + See Also + -------- + Parameter : Base parameter class. + ParameterEntry : Parameter with database key. + ParameterDatabase : Database with parameter defaulting. + + Notes + ----- + The parameter defaulting hierarchy is: + + 1. (mechanism_name, part_id, param_name) + 2. (mechanism_type, part_id, param_name) + 3. (None, part_id, param_name) + 4. (mechanism_name, None, param_name) + 5. (mechanism_type, None, param_name) + 6. (None, None, param_name) + + The `search_key` shows what was requested, while `found_key` shows + which level of defaulting was used. This information is useful for + debugging parameter lookups. + + Examples + -------- + Create a model parameter showing search and found keys: + + >>> model_param = bcp.ModelParameter( + ... 'kb', + ... 100.0, + ... search_key={'mechanism': 'binding', 'part_id': 'prom1'}, + ... found_key={'mechanism': 'binding', 'part_id': None}, + ... unit='1/s' + ... ) + >>> # Shows parameter was found using mechanism-level default """ @@ -314,6 +705,11 @@ def __init__( parameter_info=None, **kwargs, ): + """Initialize a ModelParameter object. + + See class docstring for parameter descriptions. + + """ ParameterEntry.__init__( self, parameter_name, @@ -328,44 +724,167 @@ def __init__( @property def search_key(self): + """ParameterKey: The key originally searched for in database.""" return self._search_key @search_key.setter def search_key(self, search_key): + """Set the search key. + + Parameters + ---------- + search_key : dict, ParameterKey, tuple, or str + The key that was searched for. Automatically converted to + ParameterKey namedtuple. + + """ self._search_key = self.create_parameter_key( search_key, self.parameter_name ) @property def found_key(self): + """ParameterKey: The key where parameter was actually found.""" return self._found_key @found_key.setter def found_key(self, found_key): + """Set the found key. + + Parameters + ---------- + found_key : dict, ParameterKey, tuple, or str + The key where the parameter was found after defaulting. + Automatically converted to ParameterKey namedtuple. + + """ self._found_key = self.create_parameter_key( found_key, self.parameter_name ) def __str__(self): + """Return string representation of the model parameter. + + Returns + ------- + str + String showing parameter key, value, and search key in format + 'ModelParameter() = search_key='. + + """ return ( f"ModelParameter({self.parameter_key}) = " - + f"{self.value}\tsearch_key={self.search_key}" + + f"{self.value}\n search_key={self.search_key}" ) class ParameterDatabase(object): + """Database for storing and retrieving parameters with defaulting. + + A `ParameterDatabase` stores parameters with flexible lookup keys that + enable parameter defaulting based on mechanism, part_id, and parameter + name. Parameters can be loaded from dictionaries, files, or other + databases. + + Parameters + ---------- + parameter_dictionary : dict, optional + Dictionary of parameters to load. Keys should be ParameterKey-like + (dict, tuple, or str) and values should be numerical or Parameter + objects. + parameter_file : str or list of str, optional + Path(s) to parameter file(s) to load. Files must be tab-separated + (.tsv, .txt) or comma-separated (.csv). + overwrite_parameters : bool, default=False + If True, allows overwriting existing parameters when loading. If + False, raises ValueError if duplicate keys are encountered. + + Attributes + ---------- + parameters : dict + Internal dictionary mapping ParameterKey to ParameterEntry objects. + Access via indexing or iteration rather than directly. + + See Also + -------- + Parameter : Base parameter class. + ParameterEntry : Parameter with database key. + ModelParameter : Parameter with search and found keys. + + Notes + ----- + When searching for a parameter with `find_parameter(mechanism, + part_id, param_name)`, the database searches in this order: + + 1. (mechanism.name, part_id, param_name) + 2. (mechanism.type, part_id, param_name) + 3. (None, part_id, param_name) + 4. (mechanism.name, None, param_name) + 5. (mechanism.type, None, param_name) + 6. (None, None, param_name) + + This enables flexible parameter specification where specific parameters + override more general ones. + + Parameter files should have these columns (column names are flexible): + + - 'param_name' or 'parameter_name' (required) + - 'param_val' or 'value' (required) + - 'mechanism_id' or 'mechanism' (optional) + - 'part_id' or 'part' (optional) + - 'units' or 'unit' (optional) + + Additional columns are stored in parameter_info. + + Parameter files are searched for in the following directories: + + 1. The current directory + 2. All directories listed in the 'BCP_PATH' environment variable + 3. The BioCRNpyler source code directory + + The directories are search in this order and the first parameter file that + is found is returned. For files in the BioCRNpyler source code directory, + common filename patterns are of the form '/_parameters.tsv' + where is 'components', 'mechanisms', or 'mixtures'. + + Examples + -------- + Create a parameter database from a dictionary: + + >>> params = { + ... 'kb': 100.0, + ... 'ku': 0.01, + ... ('transcription', None, 'ktx'): 0.05 + ... } + >>> db = bcp.ParameterDatabase(parameter_dictionary=params) + + Load parameters from a file: + + >>> db = bcp.ParameterDatabase( + ... parameter_file='mixtures/pure_parameters.tsv') + + Look up a parameter with defaulting: + + >>> param = db.find_parameter('transcription', 'promoter1', 'ktx') + >>> param.value + 0.05 + + Add a new parameter: + + >>> db.add_parameter('kcat', 10.0, + ... parameter_key={'mechanism': 'catalysis', 'part_id': 'enzyme1'}) + + """ + def __init__( self, parameter_dictionary=None, parameter_file=None, overwrite_parameters=False, ): - """A class for storing parameters in Components and Mixtures. + """Initialize a ParameterDatabase object. - :param parameter_dictionary: - :param parameter_file: - :param overwrite_parameters: whether to overwrite existing entries - in the parameter database + See class docstring for parameter descriptions. """ self.parameters = {} # create an emtpy dictionary to get parameters. @@ -403,6 +922,21 @@ def __init__( # To check if a key or ParameterEntry is in a the ParameterDatabase def __contains__(self, val): + """Check if a key or ParameterEntry is in the database. + + Parameters + ---------- + val : ParameterEntry, dict, ParameterKey, tuple, or str + Value to check. Can be a ParameterEntry object or any valid + parameter key format. + + Returns + ------- + bool + True if the key exists in the database (and ParameterEntry + values match if val is a ParameterEntry), False otherwise. + + """ if isinstance(val, ParameterEntry): key = val.parameter_key if key in self.parameters and self.parameters[key] == val: @@ -419,11 +953,32 @@ def __contains__(self, val): # Ability to loop through parameters eg # for entry in ParameterDatabase: ... def __iter__(self): + """Initialize iterator over parameter entries. + + Returns + ------- + ParameterDatabase + Self with iterator state initialized. + + """ self.keys = list(self.parameters.keys()) self.current_key_ind = 0 return self def __next__(self): + """Get next parameter entry in iteration. + + Returns + ------- + ParameterEntry + Next parameter entry in the database. + + Raises + ------ + StopIteration + When all parameters have been iterated. + + """ if self.current_key_ind < len(self.keys): key = self.keys[self.current_key_ind] entry = self.parameters[key] @@ -434,17 +989,64 @@ def __next__(self): # Length method def __len__(self): + """Return number of parameters in the database. + + Returns + ------- + int + Number of parameter entries stored. + + """ return len(self.parameters) # Gets a parameter from the database # Only returns exact matches. def __getitem__(self, key): + """Get a parameter by exact key match. + + Parameters + ---------- + key : dict, ParameterKey, tuple, or str + Parameter key to look up. No defaulting is performed. + + Returns + ------- + ParameterEntry + The parameter entry with the exact matching key. + + Raises + ------ + KeyError + If the exact key is not found in the database. + + """ param_key = ParameterEntry.create_parameter_key(key) return self.parameters[param_key] # Sets a parameter in the databases - useful for quickly changing # parameters, but add_parameter is recommended. def __setitem__(self, parameter_key, value): + """Set a parameter value by key. + + Parameters + ---------- + parameter_key : dict, ParameterKey, tuple, or str + Key for the parameter. + value : float, str, or ParameterEntry + New value or ParameterEntry object. If ParameterEntry, its key + must match parameter_key. + + Raises + ------ + ValueError + If value is ParameterEntry with mismatched key. + + Notes + ----- + This method automatically overwrites existing parameters. + For more control, use `add_parameter` instead. + + """ key = ParameterEntry.create_parameter_key(parameter_key) if isinstance(value, ParameterEntry): @@ -465,6 +1067,14 @@ def __setitem__(self, parameter_key, value): ) def __str__(self): + """Return string representation of the parameter database. + + Returns + ------- + str + String listing all parameters in the database. + + """ txt = 'ParameterDatabase:' param_txt = '\n'.join([repr(p) for p in self.parameters]) return txt + param_txt @@ -478,16 +1088,39 @@ def add_parameter( parameter_info=None, overwrite_parameters=False, ): - """Adds a parameter to the database with appropriate metadata. - - :param parameter_name: the name of the parameter - :param parameter_value: the value of the parameter - :param parameter_origin: - :param parameter_key: - :param parameter_info: - :param overwrite_parameters: whether to overwrite existing entries - in the parameter database - :return: + """Add a parameter to the database. + + Parameters + ---------- + parameter_name : str + Name of the parameter. + parameter_value : float or str + Value of the parameter. Strings are converted to float. + parameter_origin : str, optional + Description of where the parameter came from (e.g., filename). + Stored in `parameter_info`. + parameter_key : dict, ParameterKey, str, or None, optional + Lookup key for the parameter. If None, creates key with only + the parameter name. + parameter_info : dict, optional + Additional metadata about the parameter. `parameter_origin` is + added to this dict if provided. + overwrite_parameters : bool, default=False + If True, allows overwriting existing parameters. If False, + raises ValueError if key already exists. + + Raises + ------ + ValueError + If key already exists in database and + `overwrite_parameters=False`. + + Examples + -------- + >>> db = bcp.ParameterDatabase() + >>> db.add_parameter('kb', 100.0) + >>> db.add_parameter('ku', 0.01, + ... parameter_key={'mechanism': 'binding'}) """ # Put parameter origin into parameter_info @@ -520,12 +1153,30 @@ def load_parameters_from_dictionary( parameter_dictionary: Dict[ParameterKey, Union[str, numbers.Real]], overwrite_parameters=False, ) -> None: - """Loads Parameters from a parameter dictionary. - - :param parameter_dictionary: Dictionary with keys ParameterKey types - and values with real numbers - :param overwrite_parameters: whether to overwrite existing entries - in the parameter database + """Load parameters from a dictionary. + + Parameters + ---------- + parameter_dictionary : dict + Dictionary with keys as ParameterKey-like objects (dict, tuple, + or str) and values as numerical values or strings. + overwrite_parameters : bool, default=False + If True, allows overwriting existing parameters. If False, + raises ValueError if duplicate keys are encountered. + + Raises + ------ + ValueError + If duplicate keys exist and `overwrite_parameters=False`. + + Examples + -------- + >>> db = bcp.ParameterDatabase() + >>> params = { + ... 'kb': 100.0, + ... ('binding', None, 'ku'): 0.01, + ... } + >>> db.load_parameters_from_dictionary(params) """ for k in parameter_dictionary: @@ -544,11 +1195,30 @@ def load_parameters_from_dictionary( def load_parameters_from_database( self, parameter_database, overwrite_parameters=False ) -> None: - """Loads parameters from another ParameterDatabase. - - :param parameter_database: instance of another ParameterDatabase - :param overwrite_parameters: whether to overwrite existing entries - in the parameter database. + """Load parameters from another `ParameterDatabase`. + + Parameters + ---------- + parameter_database : ParameterDatabase + Another ParameterDatabase instance to copy parameters from. + overwrite_parameters : bool, default=False + If True, allows overwriting existing parameters. If False, + raises ValueError if duplicate keys are encountered. + + Raises + ------ + TypeError + If `parameter_database` is not a ParameterDatabase instance. + ValueError + If duplicate keys exist and `overwrite_parameters=False`. + + Examples + -------- + >>> db1 = bcp.ParameterDatabase(parameter_dictionary={'kb': 100.0}) + >>> db2 = bcp.ParameterDatabase() + >>> db2.load_parameters_from_database(db1) + >>> db2['kb'].value + 100.0 """ if not isinstance(parameter_database, ParameterDatabase): @@ -572,14 +1242,60 @@ def load_parameters_from_database( def load_parameters_from_file( self, filename: str, overwrite_parameters=False ) -> None: - """Loads parameters from a file to the ParameterDatabase. + """Load parameters from a file. - Parameter files must be tab-separated (.tsv or .txt) or - comma-separated (.csv) files! + Reads parameters from a CSV or TSV file and adds them to the + database. The file must have 'param_name' and 'param_val' columns. + Optional columns include 'mechanism', 'part_id', and 'units'. - :param filename: name of the file (with valid file path) - :param overwrite_parameters: whether to overwrite existing entries - in the parameter database + Parameters + ---------- + filename : str + Path to parameter file. Must be tab-separated (.tsv, .txt) or + comma-separated (.csv). File is searched in current directory + and BioCRNpyler package paths. + overwrite_parameters : bool, default=False + If True, allows overwriting existing parameters. If False, + raises ValueError if duplicate keys are encountered. + + Raises + ------ + ValueError + If file cannot be found, has invalid format, or contains + duplicate keys when `overwrite_parameters=False`. + + Notes + ----- + Accepted column names (case-sensitive, first match used): + + - param_name: 'parameter_name', 'parameter', 'param', 'param_name' + - param_val: 'val', 'value', 'param_val', 'parameter_value' + - mechanism: 'mechanism', 'mechanism_id' + - part_id: 'part_id', 'part' + - unit: 'units', 'unit' + + Additional columns are stored in parameter_info dictionary. + + File Format Example (CSV):: + + .. code:: + + mechanism,part_id,param_name,param_val,unit + binding,,kb,100,1/s + binding,,ku,0.01,1/s + transcription,prom1,ktx,0.05,1/s + + Examples + -------- + >>> db = bcp.ParameterDatabase( + ... parameter_file='mixtures/pure_parameters.tsv') + + Load multiple files: + + >>> db = bcp.ParameterDatabase( + ... parameter_file=[ + ... 'mixtures/pure_parameters.tsv', + ... 'components/tetr_parameters.tsv']) """ from ..utils.fileutil import find_file_in_bcp_path @@ -770,15 +1486,35 @@ def load_parameters_from_file( def _get_field_names( field_names: List[str], accepted_field_names: Dict[str, List[str]] ) -> Dict[str, str]: - """Searches through valid field names and finds currently used one. - - It builds a dictionary of currently used field names. - - :param field_names: list of field names (columns) found in the - csv file - :param accepted_field_names: dictionary of possible field names and - their valid aliases - :return: dictionary of currently used field names (aliases) + """Map parameter file column names to standard field names. + + Searches through column names in a parameter file to find which + valid aliases are being used, and creates a mapping dictionary. + + Parameters + ---------- + field_names : list of str + List of column names found in the parameter file. + accepted_field_names : dict + Dictionary mapping standard field names to lists of valid + aliases. Format: {'field': ['alias1', 'alias2', ...]}. + + Returns + ------- + dict + Dictionary mapping standard field names to the actual column + names used in the file. Fields not found are set to None. + + Raises + ------ + ValueError + If `field_names` or `accepted_field_names` are invalid types or + empty. + + Warns + ----- + UserWarning + If a standard field has no matching column in the file. """ if not isinstance(field_names, list): @@ -824,24 +1560,84 @@ def _get_field_names( return return_field_names def find_parameter(self, mechanism, part_id, param_name): - """Searches the database for the best matching parameter. - - Parameter defaulting hierarchy: - (mechanism_name, part_id, param_name) --> param_val. - If that particular parameter key cannot be found, - the software will default to the following keys: - (mechanism_type, part_id, param_name) >> (part_id, param_name) >> - (mechanism_name, param_name) >> (mechanism_type, param_name) >> - (param_name) and give a warning. - - As a note, mechanism_name refers to the .name variable of a - Mechanism. mechanism_type refers to the .type variable of a - Mechanism. Either of these can be used as a mechanism_id. This - allows for models to be constructed easily using default parameter - values and for parameters to be shared between different Mechanisms - and/or Components. + """Search for a parameter with automatic defaulting. + + Searches the database for the best matching parameter using a + hierarchical defaulting system. If an exact match is not found, + progressively more general keys are tried. + + Parameters + ---------- + mechanism : str, Mechanism, or None + Mechanism identifier. Can be a string (used as both name and + type), a Mechanism object (uses .name and .mechanism_type), or + None. + part_id : str or None + Part/component identifier for the parameter. + param_name : str + Name of the parameter to find. + + Returns + ------- + ModelParameter or None + ModelParameter object with search_key and found_key attributes + showing how the parameter was found. Returns None if no match + found at any defaulting level. + + Raises + ------ + ValueError + If `mechanism` is not a string, Mechanism object, or None. + + Notes + ----- + The method searches for parameters in this order: + + 1. (mechanism.name, part_id, param_name) + 2. (mechanism.type, part_id, param_name) + 3. (None, part_id, param_name) + 4. (mechanism.name, None, param_name) + 5. (mechanism.type, None, param_name) + 6. (None, None, param_name) + + This allows setting default parameters at various levels of + specificity. For example, a general 'kb' parameter can be + overridden for specific mechanisms or parts. + + Examples + -------- + >>> db = bcp.ParameterDatabase() + >>> db.add_parameter('kb', 100.0) + >>> db.add_parameter('kb', 200.0, + ... parameter_key={'mechanism': 'binding'}) + + General lookup finds the general parameter + + >>> param = db.find_parameter(None, None, 'kb') + >>> param.value + 100.0 + + Mechanism-specific lookup finds the specific parameter + + >>> param = db.find_parameter('binding', None, 'kb') + >>> param.value + 200.0 """ + # Parameter defaulting hierarchy: + # (mechanism_name, part_id, param_name) --> param_val. + # If that particular parameter key cannot be found, + # the software will default to the following keys: + # (mechanism_type, part_id, param_name) >> (part_id, param_name) >> + # (mechanism_name, param_name) >> (mechanism_type, param_name) >> + # (param_name) and give a warning. + # + # As a note, mechanism_name refers to the .name variable of a + # Mechanism. mechanism_type refers to the .type variable of a + # Mechanism. Either of these can be used as a mechanism_id. This + # allows for models to be constructed easily using default parameter + # values and for parameters to be shared between different Mechanisms + # and/or Components. # this is imported here because otherwise there are import loops from .mechanism import Mechanism diff --git a/biocrnpyler/core/polymer.py b/biocrnpyler/core/polymer.py index 62a49a53..958f96b1 100644 --- a/biocrnpyler/core/polymer.py +++ b/biocrnpyler/core/polymer.py @@ -1,10 +1,10 @@ """Polymer support module. -The classes OrderedPolymer and OrderedMonomer are datastructures used -to represent Polymers and their associatd components. +The classes `OrderedPolymer` and `OrderedMonomer` are data structures used +to represent Polymers and their associated components. -These classes are used by Chemical Reaction Network Species as well as certain -Components such as DNA_construct. +These classes are used by `ChemicalReactionNetwork` species as well as certain +components such as DNA_construct. """ @@ -13,7 +13,47 @@ class MonomerCollection: - """Collection of OrderedMonomers without any particular structure.""" + """Collection of ordered monomers without any particular structure. + + A base container class that holds a collection of `OrderedMonomer` + objects without imposing any ordering or structural constraints. This + class serves as a parent class for more structured polymer + representations like `OrderedPolymer`. + + Parameters + ---------- + monomers : list of OrderedMonomer + List of `OrderedMonomer` objects to include in the collection. Each + monomer is copied and linked to this collection as its parent. + + Attributes + ---------- + monomers : tuple of OrderedMonomer + Tuple containing copies of the input monomers, with each monomer's + parent set to this collection. + + See Also + -------- + OrderedPolymer : A polymer with ordered monomers and directionality. + OrderedMonomer : A unit that can belong to a MonomerCollection. + + Notes + ----- + Monomers are stored as a tuple (immutable) to prevent direct + modification. Each monomer is copied during initialization to ensure + the collection maintains its own references. + + Examples + -------- + Create a collection of monomers: + + >>> mon1 = bcp.OrderedMonomer() + >>> mon2 = bcp.OrderedMonomer() + >>> collection = bcp.MonomerCollection([mon1, mon2]) + >>> len(collection.monomers) + 2 + + """ def __init__(self, monomers): self.monomers = monomers @@ -24,6 +64,21 @@ def monomers(self): @monomers.setter def monomers(self, monomers): + """Set the monomers in the collection. + + Parameters + ---------- + monomers : list of OrderedMonomer + List of `OrderedMonomer` objects to store in the collection. + Each monomer is copied and has its parent set to this + collection. + + Raises + ------ + AssertionError + If any element in `monomers` is not an `OrderedMonomer`. + + """ mon_list = [] for monomer in monomers: assert isinstance(monomer, OrderedMonomer) @@ -34,16 +89,82 @@ def monomers(self, monomers): class OrderedPolymer(MonomerCollection): - """A polymer made up of OrderedMonomers that has a specific order.""" + """A polymer made up of OrderedMonomers with a specific order. + + Represents a linear sequence of monomers where each monomer has a + defined position and direction. This class extends `MonomerCollection` + to provide ordered, directional polymer structures commonly used to + represent DNA constructs, RNA sequences, and protein chains. + + Parameters + ---------- + parts : list or tuple + Sequence of parts to add to the polymer. Each element can be: + + - An `OrderedMonomer` object (uses existing direction) + - A list/tuple `[OrderedMonomer, direction]` specifying monomer + and its direction explicitly + + default_direction : str or int, optional + Default direction for monomers when not explicitly specified. + Common values include 'forward', 'reverse', 0, 1, or None. + + Attributes + ---------- + polymer : tuple of OrderedMonomer + Ordered tuple of monomers in this polymer. Alias for `monomers` + property inherited from `MonomerCollection`. + default_direction : str, int, or None + Default direction assigned to monomers lacking explicit direction. + + See Also + -------- + NamedPolymer : An OrderedPolymer with an associated name. + OrderedMonomer : A unit that belongs to an OrderedPolymer. + MonomerCollection : Base class for monomer collections. + + Notes + ----- + Directions indicate the orientation of monomers in the polymer: + + - 'forward' or 0: Standard/positive orientation + - 'reverse' or 1: Inverted/negative orientation + - None: No specified orientation + + The polymer tuple is immutable, but monomers can be added via + `insert`, `append`, or `replace` methods. Direct assignment to + positions uses `__setitem__` which calls `replace`. + + All monomers are deep-copied when added to ensure the polymer + maintains independent references. This prevents external modifications + from affecting the polymer structure. + + Examples + -------- + Create a polymer from monomers: + + >>> mon1 = bcp.OrderedMonomer() + >>> mon2 = bcp.OrderedMonomer() + >>> polymer = bcp.OrderedPolymer( + ... parts=[mon1, mon2], + ... default_direction='forward' + ... ) + >>> len(polymer) + 2 + + Create a polymer with explicit directions: + + >>> polymer = bcp.OrderedPolymer( + ... parts=[[mon1, 'forward'], [mon2, 'reverse']] + ... ) + >>> polymer[0].direction + 'forward' + >>> polymer[1].direction + 'reverse' - def __init__(self, parts, default_direction=None): - """Initialize an ordered polymer. + """ - Parts can be a list of lists containing - [[OrderedMonomer,direction],[OrderedMonomer,direction],...] - alternatively, you can have a regular list, and the direcitons - will end up being None - """ + def __init__(self, parts, default_direction=None): self.default_direction = default_direction self.polymer = parts @@ -53,6 +174,25 @@ def polymer(self): @polymer.setter def polymer(self, parts): + """Set the polymer sequence from a list of parts. + + Parameters + ---------- + parts : list or tuple + Sequence of parts to add to the polymer. Each element can be: + + - An `OrderedMonomer` object + - A list/tuple `[OrderedMonomer, direction]` + + Raises + ------ + AssertionError + If `parts` is not a list or tuple. + ValueError + If any element is not an `OrderedMonomer` or valid part + specification. + + """ polymer = [] assert isinstance( parts, (list, tuple) @@ -96,10 +236,51 @@ def __hash__(self): return hval def changed(self): + """Callback method invoked whenever the polymer structure changes. + + This method is called after operations that modify the polymer, + such as `insert`, `replace`, `delpart`, or `reverse`. + Subclasses can override this to implement custom behavior when + the polymer is modified. + + Notes + ----- + The base implementation does nothing. Override in subclasses to add + functionality like name regeneration, validation, or notifications. + + """ # runs whenever anything changed pass def insert(self, position, part, direction=None): + """Insert a monomer at a specific position in the polymer. + + Inserts a copy of the given monomer at the specified position, + shifting all subsequent monomers to higher positions. Calls the + `changed` callback after insertion. + + Parameters + ---------- + position : int + Index at which to insert the monomer. Must be between 0 and + `len(polymer)` (inclusive). + part : OrderedMonomer + The monomer to insert. A copy of this monomer will be added. + direction : str, int, or None, optional + Direction for the inserted monomer. If None, uses the monomer's + existing direction. + + See Also + -------- + append : Add a monomer to the end of the polymer. + replace : Replace a monomer at a specific position. + + Notes + ----- + The monomer is deep-copied before insertion to maintain + independence from the original object. + + """ # OrderedMonomers are always copied when inserted into an # OrderedPolymer part_copy = copy.copy(part) @@ -116,6 +297,34 @@ def insert(self, position, part, direction=None): self.changed() def replace(self, position, part, direction=None): + """Replace a monomer at a specific position in the polymer. + + Removes the monomer at the given position and inserts a copy of + the new monomer in its place. Calls the `changed` callback after + replacement. + + Parameters + ---------- + position : int + Index of the monomer to replace. Must be a valid position in + the polymer. + part : OrderedMonomer + The monomer to insert. A copy of this monomer will be added. + direction : str, int, or None, optional + Direction for the new monomer. If None, uses the monomer's + existing direction. + + See Also + -------- + insert : Insert a monomer at a specific position. + delpart : Remove a monomer from the polymer. + + Notes + ----- + The removed monomer's `remove` method is called to clear its + parent, position, and direction attributes. + + """ # OrderedMonomers are always copied when inserted into an # OrderedPolymer part_copy = copy.copy(part) @@ -133,6 +342,32 @@ def replace(self, position, part, direction=None): self.changed() def append(self, part, direction=None): + """Add a monomer to the end of the polymer. + + Appends a copy of the given monomer to the end of the polymer + sequence by calling `insert` at the final position. + + Parameters + ---------- + part : OrderedMonomer + The monomer to append. A copy of this monomer will be added. + direction : str, int, or None, optional + Direction for the appended monomer. If None, uses the monomer's + existing direction if available. + + See Also + -------- + insert : Insert a monomer at a specific position. + + Examples + -------- + >>> polymer = bcp.OrderedPolymer(parts=[]) + >>> mon = bcp.OrderedMonomer() + >>> polymer.append(mon, direction='forward') + >>> len(polymer) + 1 + + """ # OrderedMonomers are always copied when inserted into an # OrderedPolymer part_copy = copy.copy(part) @@ -155,6 +390,40 @@ def __repr__(self): return outstr def direction_invert(self, dirname): + """Invert a direction value. + + Converts a direction to its opposite orientation. Used during + polymer reversal operations. + + Parameters + ---------- + dirname : str, int, or None + The direction to invert. Supported values: + + - 'forward' <--> 'reverse' + - 0 <--> 1 + - None -> None + + Returns + ------- + str, int, or None + The inverted direction. Returns the input unchanged if it + cannot be inverted. + + Warns + ----- + UserWarning + If the direction value is not recognized. + + Examples + -------- + >>> polymer = bcp.OrderedPolymer(parts=[]) + >>> polymer.direction_invert('forward') + 'reverse' + >>> polymer.direction_invert(0) + 1 + + """ if dirname == 'forward': return 'reverse' elif dirname == 'reverse': @@ -170,15 +439,68 @@ def direction_invert(self, dirname): return dirname def __len__(self): + """Return the number of monomers in the polymer. + + Returns + ------- + int + The number of monomers in the polymer sequence. + + """ return len(self.polymer) def __getitem__(self, ii): + """Get a monomer or slice of monomers from the polymer. + + Parameters + ---------- + ii : int or slice + Index or slice to retrieve from the polymer. + + Returns + ------- + OrderedMonomer or tuple + The monomer at the given index, or a tuple of monomers for a + slice. + + """ return self.polymer[ii] def __setitem__(self, ii, val): + """Replace a monomer at a specific position. + + Parameters + ---------- + ii : int + Index at which to replace the monomer. + val : OrderedMonomer + The new monomer to insert at the position. + + Notes + ----- + Internally calls `replace` with the monomer's existing direction. + + """ self.replace(ii, val, val.direction) def __eq__(self, other): + """Check equality with another OrderedPolymer. + + Two polymers are equal if they have the same length and each + corresponding pair of monomers has the same direction, position, + and type. + + Parameters + ---------- + other : OrderedPolymer + The polymer to compare with. + + Returns + ------- + bool + True if polymers are equal, False otherwise. + + """ if isinstance(other, OrderedPolymer): for item1, item2 in zip(self.polymer, other.polymer): if ( @@ -194,12 +516,49 @@ def __eq__(self, other): return False def __contains__(self, item): + """Check if a monomer is in the polymer. + + Parameters + ---------- + item : OrderedMonomer + The monomer to search for. + + Returns + ------- + bool + True if the monomer is in the polymer, False otherwise. + + """ if item in self.polymer: return True else: return False def delpart(self, position): + """Remove a monomer from the polymer at a specific position. + + Removes the monomer at the given position, shifts all subsequent + monomers to lower positions, and calls the `changed` callback. + + Parameters + ---------- + position : int + Index of the monomer to remove. Must be a valid position in + the polymer. + + See Also + -------- + replace : Replace a monomer at a specific position. + insert : Insert a monomer at a specific position. + + Notes + ----- + The removed monomer's `remove` method is called to clear its + parent, position, and direction. If the polymer has a `name` + attribute and a `make_name` method, the name is regenerated + after deletion. + + """ part = self.polymer[position] part.remove() for subsequent_part in self.polymer[position + 1 :]: @@ -210,6 +569,32 @@ def delpart(self, position): self.name = self.make_name() def reverse(self): + """Reverse the order and directions of all monomers in the polymer. + + Reverses the polymer sequence and inverts the direction of each + monomer. Updates all monomer positions to reflect their new + locations. Calls the `changed` callback after reversal. + + Notes + ----- + This operation modifies the polymer in place. All monomers have + their directions inverted using `direction_invert` and their + positions updated to match the reversed sequence. + + Examples + -------- + >>> mon1 = bcp.OrderedMonomer() + >>> mon2 = bcp.OrderedMonomer() + >>> polymer = bcp.OrderedPolymer( + ... parts=[[mon1, 'forward'], [mon2, 'reverse']] + ... ) + >>> polymer.reverse() + >>> polymer[0].direction + 'forward' + >>> polymer[1].direction + 'reverse' + + """ self.polymer = self.polymer[::-1] for ind, part in enumerate(self.polymer): part.position = ind @@ -218,7 +603,75 @@ def reverse(self): class NamedPolymer(OrderedPolymer): - """The same as an OrderedPolymer but it has a name.""" + """An OrderedPolymer with an associated name and circularity flag. + + Extends `OrderedPolymer` to include a name identifier and optional + circular topology flag. Commonly used to represent named biological + constructs like plasmids, chromosomes, or specific DNA/RNA sequences. + + Parameters + ---------- + parts : list or tuple + Sequence of parts to add to the polymer. See `OrderedPolymer` for + format details. + name : str + Name identifier for the polymer. + default_direction : str, int, or None, optional + Default direction for monomers when not explicitly specified. + circular : bool, default=False + If True, indicates the polymer has circular topology (e.g., a + plasmid). If False, the polymer is linear. + + Attributes + ---------- + name : str + Name identifier for the polymer. + circular : bool + Flag indicating circular (True) or linear (False) topology. + polymer : tuple of OrderedMonomer + Ordered tuple of monomers in this polymer (inherited). + default_direction : str, int, or None + Default direction for monomers (inherited). + + See Also + -------- + OrderedPolymer : Base class for ordered polymer structures. + OrderedMonomer : A unit that belongs to an OrderedPolymer. + + Notes + ----- + The `circular` attribute is primarily informational and does not + automatically enforce circular topology constraints in polymer + operations. Subclasses or external code must handle circular semantics + as needed. + + Examples + -------- + Create a linear named polymer: + + >>> mon1 = bcp.OrderedMonomer() + >>> mon2 = bcp.OrderedMonomer() + >>> polymer = bcp.NamedPolymer( + ... parts=[mon1, mon2], + ... name='my_construct', + ... default_direction='forward' + ... ) + >>> polymer.name + 'my_construct' + >>> polymer.circular + False + + Create a circular polymer (plasmid): + + >>> plasmid = bcp.NamedPolymer( + ... parts=[mon1, mon2], + ... name='pUC19', + ... circular=True + ... ) + >>> plasmid.circular + True + + """ def __init__(self, parts, name, default_direction=None, circular=False): self.name = name @@ -231,12 +684,87 @@ def __init__(self, parts, name, default_direction=None, circular=False): class OrderedMonomer: """A unit that belongs to an OrderedPolymer. - Each unit has a direction, a location, and a link back to its parent. + Represents a single monomer unit within a polymer structure. Each + monomer tracks its position in the polymer, its directional + orientation, and maintains a reference to its parent polymer. This + class is used as a base for representing DNA parts, RNA components, + amino acids, and other polymer building blocks. + + Parameters + ---------- + direction : str, int, or None, optional + Directional orientation of the monomer in the polymer. Common + values include 'forward', 'reverse', 0, 1, or None. Default is + None. + position : int or None, optional + Index position of the monomer within its parent polymer. Must be + non-None if the monomer belongs to a polymer. Default is None. + parent : MonomerCollection or None, optional + Reference to the parent `MonomerCollection` or `OrderedPolymer` + containing this monomer. Default is None. + + Attributes + ---------- + direction : str, int, or None + Directional orientation of the monomer. + position : int or None + Position index within the parent polymer. + parent : MonomerCollection or None + Reference to the parent collection or polymer. + is_polymer_component : bool + Flag indicating whether this monomer is part of a polymer + structure. Set to True when inserted into a polymer. + + See Also + -------- + OrderedPolymer : A polymer made up of OrderedMonomers. + MonomerCollection : Base class for monomer collections. + + Notes + ----- + OrderedMonomers are deep-copied when inserted into polymers to ensure + independence. Use `get_orphan` to create a copy without a parent + reference, or `get_removed` to create a fully detached copy. + + The `is_polymer_component` flag and `find_polymer_component` method + support scenarios where monomers may be nested within complex species. + + Examples + -------- + Create a standalone monomer: + + >>> monomer = bcp.OrderedMonomer(direction='forward') + >>> monomer.direction + 'forward' + >>> monomer.parent is None + True + + Add a monomer to a polymer: + + >>> polymer = bcp.OrderedPolymer(parts=[]) + >>> monomer = bcp.OrderedMonomer() + >>> polymer.append(monomer, direction='forward') + >>> polymer[0].position + 0 + >>> polymer[0].parent is polymer + True """ def __init__(self, direction=None, position=None, parent=None): - """The default is that the monomer is not part of a polymer.""" + """Initialize an OrderedMonomer. + + Parameters + ---------- + direction : str, int, or None, optional + Directional orientation of the monomer. Default is None. + position : int or None, optional + Position index within the parent polymer. Default is None. + parent : MonomerCollection or None, optional + Reference to the parent collection. Default is None. + + """ + # The default is that the monomer is not part of a polymer. self.parent = None self.direction = None # Set position to prevent weird testing errors of not having @@ -256,6 +784,19 @@ def parent(self): @parent.setter def parent(self, parent): + """Set the parent collection for this monomer. + + Parameters + ---------- + parent : MonomerCollection or None + The parent collection or polymer to assign to this monomer. + + Raises + ------ + ValueError + If `parent` is not None and not a `MonomerCollection` instance. + + """ if parent is None or isinstance(parent, MonomerCollection): self._parent = parent else: @@ -269,6 +810,15 @@ def direction(self): @direction.setter def direction(self, direction): + """Set the directional orientation of the monomer. + + Parameters + ---------- + direction : str, int, or None + The direction to assign. Common values include 'forward', + 'reverse', 0, 1, or None. + + """ self._direction = direction @property @@ -277,12 +827,50 @@ def position(self): @position.setter def position(self, position): + """Set the position index of the monomer. + + Parameters + ---------- + position : int or None + The position index to assign. Must be non-None if the monomer + has a parent polymer. + + Raises + ------ + ValueError + If the monomer has a parent but position is None. + + """ if self.parent is not None and position is None: raise ValueError(f"{self} is part of a polymer with no position!") else: self._position = position def find_polymer_component(self): + """Find the polymer component within this monomer or its species. + + Searches this monomer and, if it is a `ComplexSpecies`, its + constituent species to find which one is marked as a polymer + component. + + Returns + ------- + OrderedMonomer or None + The monomer that is part of a polymer structure, or None if no + polymer component is found. + + Raises + ------ + ValueError + If multiple species are marked as polymer components in the + same location. + + Notes + ----- + This method is primarily used internally to handle complex species + that may contain monomers as part of larger structures. + + """ from .species import ComplexSpecies outpolymer = None @@ -309,6 +897,29 @@ def find_polymer_component(self): def monomer_insert( self, parent: OrderedPolymer, position: int, direction=None ): + """Insert this monomer into a polymer at a specific position. + + Sets the monomer's parent, position, and direction attributes to + reflect its insertion into the polymer. Marks the monomer (or its + polymer component if it is a complex species) as a polymer + component. + + Parameters + ---------- + parent : OrderedPolymer + The polymer to insert this monomer into. + position : int + The position index where this monomer is being inserted. + direction : str, int, or None, optional + The direction for this monomer. If None, uses the monomer's + existing direction. + + Raises + ------ + ValueError + If position is None, or if parent is None. + + """ if position is None: raise ValueError(f"{self} has no position to be inserted at!") if direction is None: @@ -324,10 +935,49 @@ def monomer_insert( self.direction = direction def set_dir(self, direction): + """Set the direction of the monomer and return self. + + Convenience method for setting direction in a fluent interface + style. + + Parameters + ---------- + direction : str, int, or None + The direction to assign to this monomer. + + Returns + ------- + OrderedMonomer + Returns self for method chaining. + + Examples + -------- + >>> monomer = bcp.OrderedMonomer().set_dir('forward') + >>> monomer.direction + 'forward' + + """ self.direction = direction return self def remove(self): + """Remove this monomer from its parent polymer. + + Clears the monomer's parent, position, and direction attributes, + effectively detaching it from any polymer structure. + + Returns + ------- + OrderedMonomer + Returns self for method chaining. + + See Also + -------- + get_removed : Create a fully detached copy of the monomer. + get_orphan : Create a copy with parent removed but position and + direction preserved. + + """ self.parent = None self.position = None self.direction = None @@ -335,16 +985,61 @@ def remove(self): return self def get_orphan(self): - """Returns a copy of this monomer, except with no parent. + """Create a copy of this monomer without a parent reference. + + Returns a copy that retains position and direction but has no + parent polymer. Useful for temporarily working with monomers + outside their polymer context. + + Returns + ------- + OrderedMonomer + A copy of this monomer with parent set to None but position + and direction preserved. - But it still has a position and direction. + See Also + -------- + get_removed : Create a fully detached copy. + remove : Remove this monomer from its parent in place. + + Notes + ----- + This is a shallow copy of the monomer object itself, though the + parent reference is explicitly cleared. """ + # Returns a copy of this monomer, except with no parent. + # But it still has a position and direction. copied_monomer = copy.copy(self) copied_monomer.parent = None return copied_monomer def get_removed(self): + """Create a fully detached copy of this monomer. + + Returns a copy with all polymer-related attributes (parent, + position, direction) cleared. Also removes 'forward' and 'reverse' + attributes if present. + + Returns + ------- + OrderedMonomer + A copy of this monomer with no parent, position, or direction, + and with directional attributes removed. + + See Also + -------- + get_orphan : Create a copy without parent but with position and + direction. + remove : Remove this monomer from its parent in place. + + Notes + ----- + This method is useful for creating completely independent copies of + monomers that can be reused in different contexts without any + polymer associations. + + """ copied_part = copy.copy(self) copied_part.parent = None copied_part.direction = None @@ -365,6 +1060,22 @@ def __repr__(self): return txt def __eq__(self, other): + """Check equality with another OrderedMonomer. + + Two monomers are equal if they have the same direction, position, + and parent. + + Parameters + ---------- + other : OrderedMonomer + The monomer to compare with. + + Returns + ------- + bool + True if monomers are equal, False otherwise. + + """ if isinstance(other, OrderedMonomer): if ( self.direction == other.direction @@ -375,6 +1086,15 @@ def __eq__(self, other): return False def __hash__(self): + """Compute hash value for this monomer. + + Returns + ------- + int + Hash value based on position, direction, name (if present), + and parent. + + """ hval = 0 hval += self.subhash() @@ -384,6 +1104,23 @@ def __hash__(self): return hval def subhash(self): + """Compute hash contribution from monomer properties. + + Computes a hash value based on the monomer's position, direction, + and name (if present), excluding the parent reference. + + Returns + ------- + int + Hash value based on monomer-specific properties. + + Notes + ----- + This method is used by `__hash__` to compute the monomer's hash + contribution. It excludes the parent to avoid circular dependencies + in hash computation. + + """ hval = 0 hval += hash(self.position) hval += hash(self.direction) diff --git a/biocrnpyler/core/propensities.py b/biocrnpyler/core/propensities.py index 8c8f2b20..f45e0076 100644 --- a/biocrnpyler/core/propensities.py +++ b/biocrnpyler/core/propensities.py @@ -14,19 +14,78 @@ class Propensity(object): + """Base class for reaction propensity functions in BioCRNpyler. + + Propensities define the rate laws for chemical reactions in a CRN. + Different propensity types implement different kinetic models such as + mass action, Hill functions, and custom formulas. Propensities can be + deterministic (ODE) or stochastic (Gillespie). + + Attributes + ---------- + propensity_dict : dict + Dictionary with 'species' and 'parameters' keys storing the + species and parameters used in the propensity function. + name : str or None + Name identifier for the propensity type. + + See Also + -------- + MassAction : Mass action kinetics propensity. + GeneralPropensity : Custom formula propensity. + Hill : Base class for Hill-type propensities. + + Notes + ----- + This is an abstract base class that should be subclassed to implement + specific propensity types. Subclasses must implement: + + - `create_kinetic_law`: Generate SBML kinetic law + - `pretty_print_rate`: Human-readable rate formula + + The `propensity_dict` structure: + + - 'species': {: Species object, ...} + - 'parameters': {: Parameter or number, ...} + + """ + def __init__(self): + """Initialize a Propensity object. + + Creates an empty propensity dictionary with 'species' and + 'parameters' keys. + + """ self.propensity_dict = {'species': {}, 'parameters': {}} self.name = None @staticmethod def is_valid_propensity(propensity_type) -> bool: - """Checks whether the given propensity_type is valid propensity. + """Check if an object is a valid Propensity subclass instance. + + Recursively checks all subclasses of Propensity to determine if + the given object is a valid propensity type. + + Parameters + ---------- + propensity_type : object + Object to check for Propensity validity. + + Returns + ------- + bool + True if `propensity_type` is an instance of Propensity or any + of its subclasses, False otherwise. + + Examples + -------- + >>> prop = bcp.MassAction(k_forward=100.0) + >>> bcp.Propensity.is_valid_propensity(prop) + True + >>> bcp.Propensity.is_valid_propensity("not a propensity") + False - It recursively checks all subclasses of Propensity until it finds the - propensity type. Otherwise raise a Type error - - :param propensity_type: Propensity - :returns bool """ for propensity in Propensity.get_available_propensities(): if isinstance(propensity_type, propensity): @@ -35,14 +94,23 @@ def is_valid_propensity(propensity_type) -> bool: @staticmethod def _all_subclasses(cls): - """Returns a set of all subclasses of cls (recursively calculated). + """Recursively find all subclasses of a class. + + Parameters + ---------- + cls : type + A class to find subclasses of. + + Returns + ------- + set + Set of all subclasses of `cls`, including nested subclasses. - Source: - https://stackoverflow.com/questions/3862310/ - how-to-find-all-the-subclasses-of-a-class-given-its-name + Notes + ----- + Source: https://stackoverflow.com/questions/3862310/ + how-to-find-all-the-subclasses-of-a-class-given-its-name - :param cls: A class in the codebase, for example Propensity - :return: set of all subclasses from cls """ return set(cls.__subclasses__()).union( [ @@ -54,24 +122,57 @@ def _all_subclasses(cls): @staticmethod def get_available_propensities() -> Set: + """Get all available propensity subclasses. + + Returns + ------- + set + Set of all Propensity subclass types available in BioCRNpyler. + + Examples + -------- + >>> propensities = bcp.Propensity.get_available_propensities() + >>> bcp.MassAction in propensities + True + + """ return Propensity._all_subclasses(Propensity) def _create_sbml_parameter( self, parameter_name, sbml_model, ratelaw, rename_dict=None ): - """Creates an sbml parameter for a parameter of the given name. - - if self.propensity_dict["parameter"]["parameter_name"] is a - Parameter, creates a global parameter - "Parameter.name_Parameter.part_id_Parameter.mechanism" where - part_id and mechanism can be empty (but _ will always be - incldued for uniqueness). - - if self.propensity_dict["parameter"]["parameter_name"] is a - Number, creates a local parameter "parameter_name". - - rename_dict allows for param.name to be changed to - rename_dict[param.name] + """Create an SBML parameter for the propensity. + + Creates either a global or local SBML parameter depending on + whether the parameter is a ParameterEntry or a number. + + Parameters + ---------- + parameter_name : str + Name of the parameter in propensity_dict['parameters']. + sbml_model : libsbml.Model + SBML model object to add global parameters to. + ratelaw : libsbml.KineticLaw + SBML kinetic law object to add local parameters to. + rename_dict : dict, optional + Dictionary to rename parameters. Maps original parameter names + to new names. + + Returns + ------- + libsbml.Parameter or libsbml.LocalParameter + Created SBML parameter object. + + Raises + ------ + TypeError + If parameter is not a ParameterEntry, int, or float. + + Notes + ----- + If parameter is a ParameterEntry, creates a global parameter named + '__'. If parameter is a number, + creates a local parameter with the given name. """ p = self.propensity_dict['parameters'][parameter_name] @@ -112,7 +213,28 @@ def _create_sbml_parameter( ) def _check_parameter(self, parameter, allow_None=False, positive=True): - """Helper function to set parameters and do type checking.""" + """Validate parameter type and value. + + Parameters + ---------- + parameter : Parameter, float, or None + Parameter to validate. + allow_None : bool, default=False + If True, allows None as a valid value. + positive : bool, default=True + If True, requires parameter value to be positive. + + Returns + ------- + Parameter, float, or None + The validated parameter. + + Raises + ------ + ValueError + If parameter is invalid type or has invalid value. + + """ if isinstance(parameter, Parameter) and ( parameter.value > 0 or not positive ): @@ -136,7 +258,26 @@ def _check_parameter(self, parameter, allow_None=False, positive=True): ) def _check_species(self, species, allow_None=False): - """Helper function to set species and do type checking.""" + """Validate species type. + + Parameters + ---------- + species : Species or None + Species to validate. + allow_None : bool, default=False + If True, allows None as a valid value. + + Returns + ------- + Species or None + The validated species. + + Raises + ------ + TypeError + If species is not a Species object and not None when allowed. + + """ if isinstance(species, Species): return species elif species is None and allow_None: @@ -147,17 +288,67 @@ def _check_species(self, species, allow_None=False): ) def pretty_print(self, show_parameters=True, **kwargs): + """Generate human-readable string representation of propensity. + + Parameters + ---------- + show_parameters : bool, default=True + If True, includes parameter values in output. + **kwargs + Additional keyword arguments passed to formatting methods. + + Returns + ------- + str + Formatted string showing rate formula and optionally + parameters. + + """ txt = self.pretty_print_rate(**kwargs) if show_parameters: txt += '\n' + self.pretty_print_parameters(**kwargs) return txt def pretty_print_rate(self, **kwargs): + """Generate human-readable rate formula string. + + Parameters + ---------- + **kwargs + Formatting keyword arguments (e.g., reaction, stochastic). + + Returns + ------- + str + Formatted rate formula string. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. + + """ raise NotImplementedError( "class Propensity is meant to be subclassed!" ) def pretty_print_parameters(self, show_keys=True, **kwargs): + """Generate formatted string of all propensity parameters. + + Parameters + ---------- + show_keys : bool, default=True + If True, shows search and found keys for ModelParameter + objects (useful for debugging parameter lookup). + **kwargs + Additional formatting keyword arguments. + + Returns + ------- + str + Formatted string listing all parameters and their values. + + """ txt = '' for k in self.propensity_dict['parameters']: p = self.propensity_dict['parameters'][k] @@ -179,44 +370,130 @@ def pretty_print_parameters(self, show_keys=True, **kwargs): @property def is_reversible(self): - """By default, Propensities are assumed to NOT be reversible.""" + """bool: Whether the propensity represents a reversible reaction. + + Default is False. Subclasses override this for reversible kinetics. + + """ return False @property def k_forward(self): + """Float : Forward rate constant. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses that use rate constants. + + """ raise NotImplementedError @property def k_reverse(self): + """Float or None: Reverse rate constant for reversible reactions. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses that use rate constants. + + """ raise NotImplementedError def __eq__(self, other): + """Test equality between propensities. + + Parameters + ---------- + other : Propensity + Other propensity to compare with. + + Returns + ------- + bool + True if propensities have the same class and propensity_dict. + + """ if other.__class__ == self.__class__: return other.propensity_dict == self.propensity_dict @property def species(self) -> List: - """Returns the instance variables that are species type.""" + """List of Species : All species used in the propensity function.""" return list(self.propensity_dict['species'].values()) def create_kinetic_law( self, reaction, reverse_reaction, stochastic, **kwargs ): + """Create SBML kinetic law for a reaction. + + Parameters + ---------- + reaction : libsbml.Reaction + SBML reaction object to add kinetic law to. + reverse_reaction : bool + If True, creates kinetic law for reverse direction. + stochastic : bool + If True, uses stochastic propensity formulation. + **kwargs + Additional arguments (e.g., crn_reaction, model). + + Returns + ------- + libsbml.KineticLaw + Created SBML kinetic law object. + + Raises + ------ + NotImplementedError + Must be implemented by subclasses. + + """ raise NotImplementedError( "class Propensity is meant to be subclassed!" ) @classmethod def from_dict(cls, propensity_dict): + """Create a propensity from a dictionary. + + Parameters + ---------- + propensity_dict : dict + Dictionary with 'parameters' and 'species' keys containing + parameter and species values. + + Returns + ------- + Propensity + New instance of the propensity class. + + """ merged = propensity_dict['parameters'] merged.update(propensity_dict['species']) return cls(**merged) def _create_annotation(self, model, propensity_dict_in_sbml, **kwargs): - """Create simulator specific annotations to part of SBML model object. - - Annotations are used to take advantage of a simulator specific - need/feature. + """Create simulator-specific annotations for SBML model. + + Annotations enable simulator-specific features and optimizations. + Currently supports bioscrape annotations. + + Parameters + ---------- + model : libsbml.Model + SBML model object. + propensity_dict_in_sbml : dict + Propensity dictionary with SBML identifiers. + **kwargs + Additional arguments. If 'for_bioscrape' is True, creates + bioscrape annotations. + + Returns + ------- + str + XML annotation string to append to SBML reaction. """ annotation_string = '' @@ -237,10 +514,27 @@ def _create_annotation(self, model, propensity_dict_in_sbml, **kwargs): return annotation_string def _create_bioscrape_annotation(self, propensity_dict_in_sbml): - """Add BioSCRAPE annotation. + """Generate bioscrape-specific propensity type annotation. + + Creates XML annotation that bioscrape uses to optimize simulation + by identifying propensity types. - Propensity Annotations are Used to take advantage of Bioscrape - Propensity types for faster simulation. + Parameters + ---------- + propensity_dict_in_sbml : dict + Propensity dictionary with SBML identifiers for species and + parameters. + + Returns + ------- + str + XML annotation string with bioscrape PropensityType tag. + + Notes + ----- + Bioscrape uses propensity type annotations for faster simulation. + The annotation includes parameter and species names with their + SBML identifiers, plus the propensity type name. """ annotation_dict = defaultdict() @@ -270,6 +564,25 @@ def _create_bioscrape_annotation(self, propensity_dict_in_sbml): return annotation_string def _translate_propensity_dict_to_sbml(self, model, ratelaw): + """Translate internal propensity representation to SBML format. + + Converts propensity_dict with Species objects and Parameters to + SBML identifiers that can be used in SBML rate formulas. + + Parameters + ---------- + model : libsbml.Model + SBML model object to add global parameters to. + ratelaw : libsbml.KineticLaw + SBML kinetic law object to add local parameters to. + + Returns + ------- + dict + Copy of propensity_dict with species and parameters replaced + by their SBML identifier strings. + + """ # get copy of the propensity_dict and fill with sbml names propensity_dict_in_sbml = copy.deepcopy(self.propensity_dict) for param_name in propensity_dict_in_sbml['parameters'].keys(): @@ -289,22 +602,88 @@ def _translate_propensity_dict_to_sbml(self, model, ratelaw): class GeneralPropensity(Propensity): + """Propensity with user-defined formula string. + + A `GeneralPropensity` allows specification of arbitrary kinetic rate + laws using a formula string. The formula can reference species and + parameters that are validated and tracked. + + Parameters + ---------- + propensity_function : str + Valid mathematical formula as a string (e.g., 'k*S1*S2'). Must + contain all referenced species and parameters. + propensity_species : list of Species + List of Species objects used in the formula. Each species must + appear in the propensity_function string. + propensity_parameters : list of ParameterEntry + List of ParameterEntry objects used in the formula. Each + parameter name must appear in the propensity_function string. + + Attributes + ---------- + propensity_function : str + The mathematical formula defining the rate law. + name : str + Set to 'general' for this propensity type. + + Raises + ------ + TypeError + If propensity_species or propensity_parameters contain invalid + types. + ValueError + If species or parameters in lists do not appear in the formula. + + See Also + -------- + MassAction : Mass action kinetics propensity. + Hill : Hill-type propensities. + + Notes + ----- + The propensity_function string must be a valid mathematical expression + that can be parsed by libsbml.parseL3Formula(). It can include: + + - Arithmetic operators: `+, -, *, /, ^` + - Mathematical functions: exp, log, sin, cos, etc. + - Species names (as strings matching their representation) + - Parameter names (matching ParameterEntry.parameter_name) + + Examples + -------- + Create a custom Michaelis-Menten propensity: + + >>> S = bcp.Species('S') + >>> E = bcp.Species('E') + >>> kcat = bcp.ParameterEntry('kcat', 0.1) + >>> Km = bcp.ParameterEntry('Km', 10.0) + >>> prop = bcp.GeneralPropensity( + ... propensity_function='kcat*E*S/(Km + S)', + ... propensity_species=[S, E], + ... propensity_parameters=[kcat, Km] + ... ) + + Create a custom regulatory function: + + >>> X = bcp.Species('X') + >>> Y = bcp.Species('Y') + >>> k1 = bcp.ParameterEntry('k1', 1.0) + >>> k2 = bcp.ParameterEntry('k2', 0.5) + >>> prop = bcp.GeneralPropensity( + ... propensity_function='k1*X + k2*Y^2', + ... propensity_species=[X, Y], + ... propensity_parameters=[k1, k2] + ... ) + + """ + def __init__( self, propensity_function: str, propensity_species: List[Species], propensity_parameters: List[ParameterEntry], ): - """A class to define a general propensity. - - :param propensity_function: valid propensity formula defined as a - string - :param propensity_species: list of species that are part of - the propensity_function - :param propensity_parameters: list of parameters that are part of - the propensity_function - - """ super(GeneralPropensity, self).__init__() self.propensity_function = propensity_function @@ -343,10 +722,43 @@ def __init__( self.name = 'general' def pretty_print_rate(self, **kwargs): + """Return the propensity function formula string. + + Returns + ------- + str + The propensity_function formula. + + """ return self.propensity_function def create_kinetic_law(self, model, sbml_reaction, **kwargs): - """Creates SBML KineticLaw using the propensity_function string.""" + """Create SBML kinetic law using the propensity_function string. + + Translates species and parameter names to SBML identifiers and + creates the kinetic law from the formula string. + + Parameters + ---------- + model : libsbml.Model + SBML model object. + sbml_reaction : libsbml.Reaction + SBML reaction to add kinetic law to. + **kwargs + Additional keyword arguments (unused for GeneralPropensity). + + Returns + ------- + libsbml.KineticLaw + Created SBML kinetic law object. + + Raises + ------ + ValueError + If the propensity_function cannot be parsed as valid SBML + formula. + + """ ratelaw = sbml_reaction.createKineticLaw() propensity_dict_in_sbml = self._translate_propensity_dict_to_sbml( @@ -380,6 +792,83 @@ def create_kinetic_law(self, model, sbml_reaction, **kwargs): class MassAction(Propensity): + r"""Mass action kinetics propensity. + + Implements mass action rate laws for chemical reactions. Supports both + irreversible and reversible reactions. Propensities are computed + differently for deterministic (ODE) and stochastic (Gillespie) + simulations. + + Parameters + ---------- + k_forward : float or ParameterEntry + Forward reaction rate constant. Must be positive. + k_reverse : float, ParameterEntry, or None, optional + Reverse reaction rate constant. If None, reaction is irreversible. + If provided, must be positive. + + Attributes + ---------- + name : str + Set to 'massaction' for this propensity type. + + See Also + -------- + GeneralPropensity : Custom formula propensity. + Hill : Hill-type propensities. + + Notes + ----- + Deterministic (ODE) propensity: For reaction A + B --> C with rate + constant k: + + .. math:: + + \text{rate} = k [A] [B] + + Stochastic (Gillespie) propensity: For reaction A + B --> C with + rate constant k: + + .. math:: + + \text{propensity} &= k \cdot A \cdot (B-1) \text{ if } A=B \\ + \text{propensity} &= k \cdot A \cdot B \text{ otherwise} + + The stochastic formulation accounts for combinatorics of molecule + selection. For stoichiometric coefficient n > 1: + + .. math:: + + \text{factor} = S \cdot (S-1) \cdot ... \cdot (S-n+1) + + If `k_reverse` is provided, the reaction is reversible: + + .. math:: + + A + B \rightleftharpoons C + + Two kinetic laws are created: one for forward, one for reverse. + + Examples + -------- + Create an irreversible mass action propensity: + + >>> prop = bcp.MassAction(k_forward=100.0) + + Create a reversible mass action propensity: + + >>> prop = bcp.MassAction(k_forward=100.0, k_reverse=0.01) + >>> prop.is_reversible + True + + Use with ParameterEntry objects: + + >>> kb = bcp.ParameterEntry('kb', 100.0, unit='1/(nM*s)') + >>> ku = bcp.ParameterEntry('ku', 0.01, unit='1/s') + >>> prop = bcp.MassAction(k_forward=kb, k_reverse=ku) + + """ + def __init__( self, k_forward: Union[float, ParameterEntry], @@ -392,6 +881,7 @@ def __init__( @property def k_forward(self): + """float: Forward rate constant value.""" if isinstance(self._k_forward, Parameter): return self._k_forward.value else: @@ -399,11 +889,20 @@ def k_forward(self): @k_forward.setter def k_forward(self, new_k_forward): + """Set the forward rate constant. + + Parameters + ---------- + new_k_forward : float or ParameterEntry + New forward rate constant. Must be positive. + + """ self._k_forward = self._check_parameter(new_k_forward) self.propensity_dict['parameters']['k_forward'] = self._k_forward @property def k_reverse(self): + """float: Reverse rate constant value, None if irreversible.""" if isinstance(self._k_reverse, Parameter): return self._k_reverse.value else: @@ -411,6 +910,15 @@ def k_reverse(self): @k_reverse.setter def k_reverse(self, new_k_reverse): + """Set the reverse rate constant. + + Parameters + ---------- + new_k_reverse : float, ParameterEntry, or None + New reverse rate constant. If None, reaction is irreversible. + If provided, must be positive. + + """ self._k_reverse = self._check_parameter( new_k_reverse, allow_None=True ) @@ -419,12 +927,28 @@ def k_reverse(self, new_k_reverse): @property def is_reversible(self): + """bool: True if k_reverse is not None, False otherwise.""" if self.k_reverse is None: return False else: return True def pretty_print_rate(self, **kwargs): + """Generate human-readable rate formula string. + + Parameters + ---------- + **kwargs + Must include 'reaction' (CRN Reaction object) and 'stochastic' + (bool) keys. + + Returns + ------- + str + Formatted rate formula showing forward rate and optionally + reverse rate. + + """ crn_reaction = kwargs['reaction'] reactant_species = {} for w_species in crn_reaction.inputs: @@ -446,10 +970,43 @@ def create_kinetic_law( model, sbml_reaction, stochastic, + crn_reaction=None, reverse_reaction=False, **kwargs, ): - if (crn_reaction := kwargs.pop('crn_reaction', None)) is None: + """Create SBML kinetic law for mass action reaction. + + Generates SBML kinetic law with proper mass action formula for + either forward or reverse direction. + + Parameters + ---------- + model : libsbml.Model + SBML model object. + sbml_reaction : libsbml.Reaction + SBML reaction to add kinetic law to. + stochastic : bool + If True, uses stochastic mass action formula accounting for + combinatorics. + crn_reaction : Reaction + Mass action reaction to use for the kinetic law. + reverse_reaction : bool, default=False + If True, creates kinetic law for reverse reaction. + **kwargs + Must include 'crn_reaction' (CRN Reaction object). + + Returns + ------- + libsbml.KineticLaw + Created SBML kinetic law object. + + Raises + ------ + ValueError + If crn_reaction not provided or if rate formula is invalid. + + """ + if crn_reaction is None: raise ValueError( "crn_reaction reference is needed for Massaction kinetics!" ) @@ -505,6 +1062,34 @@ def create_kinetic_law( def _get_rate_formula( self, rate_coeff_name, stochastic, reactant_species ) -> str: + """Generate mass action rate formula string. + + Creates the mathematical formula for mass action kinetics, + accounting for stoichiometry and stochastic vs deterministic + simulation. + + Parameters + ---------- + rate_coeff_name : str + Name of the rate constant parameter (SBML identifier). + stochastic : bool + If True, uses stochastic formula with combinatorics. If False, + uses deterministic ODE formula. + reactant_species : dict + Dictionary mapping species_id (str) to WeightedSpecies objects + with stoichiometry information. + + Returns + ------- + str + Rate formula string suitable for SBML parseL3Formula(). + + Notes + ----- + For deterministic: rate = k * [A]^n * [B]^m + For stochastic: rate = k * A * (A-1) * ... * (A-n+1) * B * ... + + """ # Create Rate-strings for massaction propensities ratestring = rate_coeff_name @@ -533,6 +1118,60 @@ def _get_rate_formula( class Hill(Propensity): + """Base class for Hill-type propensities. + + Hill propensities implement cooperative binding kinetics with + sigmoidal response curves. This base class provides common + functionality for positive and negative Hill functions. + + Parameters + ---------- + k : float or ParameterEntry + Maximum rate constant. Must be positive. + s1 : Species + Input species that drives the Hill function. + K : float or ParameterEntry + Half-saturation (dissociation) constant. Must be positive. + n : float or ParameterEntry + Hill coefficient (cooperativity). Must be positive. Values > 1 + indicate positive cooperativity, < 1 negative cooperativity. + d : Species or None + Optional species for proportional Hill functions. If provided, + rate is proportional to this species concentration. + + Attributes + ---------- + k : float + Maximum rate constant value. + K : float + Half-saturation constant value. + n : float + Hill coefficient value. + s1 : Species + Input species. + d : Species or None + Proportional species (None for non-proportional Hill). + + See Also + -------- + HillPositive : Positive Hill function. + HillNegative : Negative Hill function (repression). + ProportionalHillPositive : Proportional positive Hill. + ProportionalHillNegative : Proportional negative Hill. + + Notes + ----- + This is an abstract base class. Use the specific subclasses: + + - `HillPositive`: Activation, :math:`k s_1^n / (K^n + s_1^n)` + - `HillNegative`: Repression, :math:`k / (1 + (s_1/K)^n)` + - `ProportionalHillPositive`: :math:`k d s_1^n / (K^n + s_1^n)` + - `ProportionalHillNegative`: :math:`k d / (1 + (s_1/K)^n)` + + Hill functions are not reversible - k_reverse is not supported. + + """ + def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): Propensity.__init__(self) self.k = k @@ -544,6 +1183,7 @@ def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): @property def k(self): + """float: Maximum rate constant value.""" if isinstance(self._k, Parameter): return self._k.value else: @@ -551,11 +1191,20 @@ def k(self): @k.setter def k(self, new_k): + """Set the maximum rate constant. + + Parameters + ---------- + new_k : float or ParameterEntry + New maximum rate constant. Must be positive. + + """ self._k = self._check_parameter(new_k) self.propensity_dict['parameters']['k'] = self._k @property def K(self): + """float: Half-saturation (dissociation) constant value.""" if isinstance(self._K, Parameter): return self._K.value else: @@ -563,11 +1212,20 @@ def K(self): @K.setter def K(self, new_K): + """Set the half-saturation constant. + + Parameters + ---------- + new_K : float or ParameterEntry + New half-saturation constant. Must be positive. + + """ self._K = self._check_parameter(new_K) self.propensity_dict['parameters']['K'] = self._K @property def n(self): + """float: Hill coefficient (cooperativity) value.""" if isinstance(self._n, Parameter): return self._n.value else: @@ -575,28 +1233,64 @@ def n(self): @n.setter def n(self, new_n): + """Set the Hill coefficient. + + Parameters + ---------- + new_n : float or ParameterEntry + New Hill coefficient. Must be positive. + + """ self._n = self._check_parameter(new_n) self.propensity_dict['parameters']['n'] = self._n @property def s1(self): + """Species: Input species driving the Hill function.""" return self._s1 @s1.setter def s1(self, new_s1): + """Set the input species. + + Parameters + ---------- + new_s1 : Species + New input species. + + """ self._s1 = self._check_species(new_s1) self.propensity_dict['species']['s1'] = self.s1 @property def d(self): + """Species: Proportional species (None if not proportional).""" return self._d @d.setter def d(self, new_d): + """Set the proportional species. + + Parameters + ---------- + new_d : Species or None + New proportional species. None for non-proportional Hill. + + """ self._d = self._check_species(new_d, allow_None=True) self.propensity_dict['species']['d'] = self._d def pretty_print_rate(self, show_parameters=True, **kwargs): + """Generate human-readable rate formula string. + + Raises + ------ + NotImplementedError + Hill base class doesn't have a rate formula. Use subclasses + HillPositive, HillNegative, ProportionalHillPositive, or + ProportionalHillNegative. + + """ raise NotImplementedError( "Propensity class Hill is meant to be subclassed: " "try HillPositive, HillNegative, ProportionalHillPositive, " @@ -604,7 +1298,35 @@ def pretty_print_rate(self, show_parameters=True, **kwargs): ) def create_kinetic_law(self, model, sbml_reaction, stochastic, **kwargs): - """This code is reused in all Hill Propensity subclasses.""" + """Create SBML kinetic law for Hill propensity. + + This method is shared by all Hill subclasses. + + Parameters + ---------- + model : libsbml.Model + SBML model object. + sbml_reaction : libsbml.Reaction + SBML reaction to add kinetic law to. + stochastic : bool + If True, uses stochastic formulation (same as deterministic + for Hill functions). + **kwargs + Additional arguments. 'reverse_reaction' is not supported. + + Returns + ------- + libsbml.KineticLaw + Created SBML kinetic law object. + + Raises + ------ + ValueError + If reverse_reaction=True (Hill propensities cannot be + reversible) or if rate formula is invalid. + + """ + # This code is reused in all Hill Propensity subclasses. if ( 'reverse_reaction' in kwargs and kwargs['reverse_reaction'] is True @@ -640,35 +1362,121 @@ def create_kinetic_law(self, model, sbml_reaction, stochastic, **kwargs): return ratelaw def _get_rate_formula(self, propensity_dict): + """Generate Hill rate formula string. + + Parameters + ---------- + propensity_dict : dict + Propensity dictionary with SBML identifiers. + + Returns + ------- + str + Rate formula string. + + Raises + ------ + NotImplementedError + Hill base class doesn't have a rate formula. Use subclasses. + + """ raise NotImplementedError( "Hill does not have a rate formula! Check out the subclasses." ) class HillPositive(Hill): - def __init__(self, k: float, s1: Species, K: float, n: float): - """Positive propensity function for a Hill rate law. + r"""Positive Hill function propensity (activation). - Hill positive propensity is a nonlinear propensity with the - following formula. + Implements an activating Hill function with sigmoidal dose-response + curve. As s1 increases, the rate approaches k. - p(s1; k, K, n) = k*s1^n/(s1^n + K) + Parameters + ---------- + k : float or ParameterEntry + Maximum rate constant. Must be positive. + s1 : Species + Input species that activates the reaction. + K : float or ParameterEntry + Half-saturation constant (concentration at half-maximal rate). + Must be positive. + n : float or ParameterEntry + Hill coefficient (cooperativity). Must be positive. Values > 1 + indicate ultrasensitivity. - :param k: rate constant (float) - :param s1: species (chemical_reaction_network.species) - :param K: dissociation constant (float) + Attributes + ---------- + name : str + Set to 'hillpositive' for this propensity type. - """ + See Also + -------- + HillNegative : Repressive Hill function. + ProportionalHillPositive : Proportional positive Hill. + + Notes + ----- + The following formula is implemented: + + .. math:: + + p(s_1; k, K, n) = \frac{k s_1^n}{K^n + s_1^n}, + + leading to the following behaviors: + + - When s1 = 0: rate ≈ 0 + - When s1 = K: rate = k/2 + - When s1 >> K: rate -> k + - Larger n gives sharper (more switch-like) response + + Examples + -------- + Create a Hill activation propensity: + + >>> X = bcp.Species('X') + >>> prop = bcp.HillPositive(k=10.0, s1=X, K=50.0, n=2.0) + + Use with parameter objects: + + >>> kmax = bcp.ParameterEntry('kmax', 10.0) + >>> Kd = bcp.ParameterEntry('Kd', 50.0) + >>> hill_n = bcp.ParameterEntry('n', 2.0) + >>> prop = bcp.HillPositive(k=kmax, s1=X, K=Kd, n=hill_n) + + """ + + def __init__(self, k: float, s1: Species, K: float, n: float): Hill.__init__(self=self, k=k, s1=s1, K=K, n=n, d=None) self.name = 'hillpositive' def pretty_print_rate(self, show_parameters=True, **kwargs): + """Generate human-readable rate formula string. + + Returns + ------- + str + Formatted Hill positive formula. + + """ return ( f" Kf = k {self.s1.pretty_print(**kwargs)}^n / " + f"( K^n + {self.s1.pretty_print(**kwargs)}^n )" ) def _get_rate_formula(self, propensity_dict): + """Generate SBML-compatible Hill positive rate formula. + + Parameters + ---------- + propensity_dict : dict + Propensity dictionary with SBML identifiers. + + Returns + ------- + str + SBML rate formula string. + + """ k = propensity_dict['parameters']['k'] n = propensity_dict['parameters']['n'] K = propensity_dict['parameters']['K'] @@ -678,27 +1486,94 @@ def _get_rate_formula(self, propensity_dict): class HillNegative(Hill): - def __init__(self, k: float, s1: Species, K: float, n: float): - """Propensity function for a negative Hill rate law. + r"""Negative Hill function propensity (repression). - Hill negative propensity is a nonlinear propensity with the - following formula: + Implements a repressive Hill function. As s1 increases, the rate + decreases from k toward zero. - p(s1; k, K, n) = k*1/(s1^n + K) + Parameters + ---------- + k : float or ParameterEntry + Maximum rate constant (when s1=0). Must be positive. + s1 : Species + Input species that represses the reaction. + K : float or ParameterEntry + Half-saturation constant (concentration at half-maximal + repression). Must be positive. + n : float or ParameterEntry + Hill coefficient (cooperativity). Must be positive. Values > 1 + indicate ultrasensitive repression. - :param k: rate constant (float) - :param s1: species (chemical_reaction_network.species) - :param K: dissociation constant (float) - :param n: cooperativity (float) + Attributes + ---------- + name : str + Set to 'hillnegative' for this propensity type. - """ + See Also + -------- + HillPositive : Activating Hill function. + ProportionalHillNegative : Proportional negative Hill. + + Notes + ----- + The following mathematical formula is implemented: + + .. math:: + + p(s_1; k, K, n) = \frac{k}{1 + (s_1/K)^n} + + leading to the following behavior: + + - When s1 = 0: rate = k + - When s1 = K: rate = k/2 + - When s1 >> K: rate -> 0 + - Larger n gives sharper (more switch-like) repression + + Examples + -------- + Create a Hill repression propensity: + + >>> R = bcp.Species('R') # Repressor + >>> prop = bcp.HillNegative(k=10.0, s1=R, K=50.0, n=2.0) + + Model transcriptional repression: + + >>> repressor = bcp.Species('repressor') + >>> kmax = bcp.ParameterEntry('kmax', 1.0) + >>> Ki = bcp.ParameterEntry('Ki', 100.0) + >>> prop = bcp.HillNegative(k=kmax, s1=repressor, K=Ki, n=2.0) + + """ + + def __init__(self, k: float, s1: Species, K: float, n: float): Hill.__init__(self=self, k=k, s1=s1, K=K, n=n, d=None) self.name = 'hillnegative' def pretty_print_rate(self, show_parameters=True, **kwargs): + """Generate human-readable rate formula string. + + Returns + ------- + str + Formatted Hill negative formula. + + """ return f" Kf = k / ( 1 + ({self.s1.pretty_print(**kwargs)}/K)^n )" def _get_rate_formula(self, propensity_dict): + """Generate SBML-compatible Hill negative rate formula. + + Parameters + ---------- + propensity_dict : dict + Propensity dictionary with SBML identifiers. + + Returns + ------- + str + SBML rate formula string. + + """ k = propensity_dict['parameters']['k'] n = propensity_dict['parameters']['n'] K = propensity_dict['parameters']['K'] @@ -708,23 +1583,90 @@ def _get_rate_formula(self, propensity_dict): class ProportionalHillPositive(HillPositive): - def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): - """Propensity function for a proportional Hill runciton rate law. + r"""Proportional positive Hill function propensity. - Proportional Hill positive propensity with the following formula: + Implements a positive Hill function with rate proportional to a + species concentration. Commonly used for regulated production where + the rate depends on both an activator and a template/enzyme. - p(s1, d; k, K, n) = k*d*s1^n/(s1^n + K) + Parameters + ---------- + k : float or ParameterEntry + Maximum rate constant per unit of d. Must be positive. + s1 : Species + Input species that activates the reaction (e.g., transcription + factor). + K : float or ParameterEntry + Half-saturation constant for s1. Must be positive. + n : float or ParameterEntry + Hill coefficient (cooperativity). Must be positive. + d : Species + Proportional species (e.g., DNA template, enzyme). Rate scales + linearly with this species concentration. - :param k: rate constant (float) - :param s1: species (chemical_reaction_network.species) - :param K: dissociation constant (float) - :param n: cooperativity (float) - :param d: species (chemical_reaction_network.species) - """ + Attributes + ---------- + name : str + Set to 'proportionalhillpositive' for this propensity type. + + See Also + -------- + HillPositive : Non-proportional positive Hill. + ProportionalHillNegative : Proportional negative Hill. + + Notes + ----- + The following mathematical formula: is used for the popensity: + + .. math:: + + p(s_1, d; k, K, n) = \frac{k d s_1^n}{K^n + s_1^n} + + This is commonly used for transcription, where + + - d = DNA template concentration + - s1 = transcription factor concentration + - Rate is proportional to both template and TF activation + + This results in the following behaviors: + + - When d = 0: rate = 0 (no template/enzyme) + - When s1 = 0: rate ≈ 0 (no activation) + - When s1 >> K: rate -> k*d (fully activated, proportional to d) + + Examples + -------- + Model regulated transcription: + + >>> TF = bcp.Species('TF') # Transcription factor + >>> DNA = bcp.Species('DNA') # DNA template + >>> prop = bcp.ProportionalHillPositive( + ... k=0.1, s1=TF, K=50.0, n=2.0, d=DNA) + + Model enzyme with allosteric activator: + + >>> activator = bcp.Species('activator') + >>> enzyme = bcp.Species('enzyme') + >>> kcat = bcp.ParameterEntry('kcat', 10.0) + >>> Ka = bcp.ParameterEntry('Ka', 100.0) + >>> prop = bcp.ProportionalHillPositive( + ... k=kcat, s1=activator, K=Ka, n=2.0, d=enzyme) + + """ + + def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): Hill.__init__(self=self, k=k, s1=s1, K=K, n=n, d=d) self.name = 'proportionalhillpositive' def pretty_print_rate(self, show_parameters=True, **kwargs): + """Generate human-readable rate formula string. + + Returns + ------- + str + Formatted proportional Hill positive formula. + + """ return ( f" Kf = k {self.d.pretty_print(**kwargs)} " + f"{self.s1.pretty_print(**kwargs)}^n / " @@ -732,6 +1674,19 @@ def pretty_print_rate(self, show_parameters=True, **kwargs): ) def _get_rate_formula(self, propensity_dict): + """Generate SBML-compatible proportional Hill positive formula. + + Parameters + ---------- + propensity_dict : dict + Propensity dictionary with SBML identifiers. + + Returns + ------- + str + SBML rate formula string. + + """ k = propensity_dict['parameters']['k'] n = propensity_dict['parameters']['n'] K = propensity_dict['parameters']['K'] @@ -741,30 +1696,108 @@ def _get_rate_formula(self, propensity_dict): class ProportionalHillNegative(HillNegative): - def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): - """Proportional Hill negative propensity. + r"""Proportional negative Hill function propensity. - Satisfies the following formula: + Implements a repressive Hill function with rate proportional to a + species concentration. Commonly used for regulated production where + a repressor inhibits production from a template/enzyme. - p(s1, d; k, K, n) = k*d/(s1^n + K) + Parameters + ---------- + k : float or ParameterEntry + Maximum rate constant per unit of d (when s1=0). Must be positive. + s1 : Species + Input species that represses the reaction (e.g., repressor). + K : float or ParameterEntry + Half-saturation constant for repression by s1. Must be positive. + n : float or ParameterEntry + Hill coefficient. Must be positive. + d : Species + Proportional species (e.g., DNA template, enzyme). Rate scales + linearly with this species concentration. - :param k: rate constant (float) - :param s1: species (chemical_reaction_network.species) - :param K: dissociation constant (float) - :param n: cooperativity (float) - :param d: species (chemical_reaction_network.species) + Attributes + ---------- + name : str + Set to 'proportionalhillnegative' for this propensity type. - """ + See Also + -------- + HillNegative : Non-proportional negative Hill. + ProportionalHillPositive : Proportional positive Hill. + + Notes + ----- + The following mathematical formula: is used: + + .. math:: + + p(s_1, d; k, K, n) = \frac{k d}{1 + (s_1/K)^n} + + This is commonly used for repressed transcription where + + - d = DNA template concentration + - s1 = repressor concentration + - Rate is proportional to template but repressed by s1 + + and resulting in the following behaviors: + + - When d = 0: rate = 0 (no template/enzyme) + - When s1 = 0: rate = k*d (fully derepressed) + - When s1 >> K: rate -> 0 (fully repressed) + + Examples + -------- + Model repressed transcription: + + >>> repressor = bcp.Species('repressor') + >>> DNA = bcp.Species('DNA') + >>> prop = bcp.ProportionalHillNegative( + ... k=0.1, s1=repressor, K=50.0, n=2.0, d=DNA) + + Model enzyme with allosteric inhibitor: + + >>> inhibitor = bcp.Species('inhibitor') + >>> enzyme = bcp.Species('enzyme') + >>> kcat = bcp.ParameterEntry('kcat', 10.0) + >>> Ki = bcp.ParameterEntry('Ki', 100.0) + >>> prop = bcp.ProportionalHillNegative( + ... k=kcat, s1=inhibitor, K=Ki, n=2.0, d=enzyme) + + """ + + def __init__(self, k: float, s1: Species, K: float, n: float, d: Species): Hill.__init__(self=self, k=k, s1=s1, K=K, n=n, d=d) self.name = 'proportionalhillnegative' def pretty_print_rate(self, show_parameters=True, **kwargs): + """Generate human-readable rate formula string. + + Returns + ------- + str + Formatted proportional Hill negative formula. + + """ return ( f" Kf = k {self.d.pretty_print(**kwargs)} / " + f"( 1 + ({self.s1.pretty_print(**kwargs)}/K)^{self.n} )" ) def _get_rate_formula(self, propensity_dict): + """Generate SBML-compatible proportional Hill negative formula. + + Parameters + ---------- + propensity_dict : dict + Propensity dictionary with SBML identifiers. + + Returns + ------- + str + SBML rate formula string. + + """ k = propensity_dict['parameters']['k'] n = propensity_dict['parameters']['n'] K = propensity_dict['parameters']['K'] diff --git a/biocrnpyler/core/reaction.py b/biocrnpyler/core/reaction.py index b96c0f3e..1389bfd2 100644 --- a/biocrnpyler/core/reaction.py +++ b/biocrnpyler/core/reaction.py @@ -13,17 +13,98 @@ class Reaction(object): - r"""An abstract representation of a chemical reaction in a CRN. - + r"""Chemical reaction in a CRN with species and rate law. + + A `Reaction` represents a chemical transformation between species with + an associated propensity (rate law). Reactions can be irreversible or + reversible, and support various kinetic models through different + propensity types. + + Parameters + ---------- + inputs : list of Species or list of WeightedSpecies + Reactant species. Can be Species objects (stoichiometry=1) or + WeightedSpecies objects (with custom stoichiometry). Duplicates + are automatically combined. + outputs : list of Species or list of WeightedSpecies + Product species. Can be Species objects (stoichiometry=1) or + WeightedSpecies objects (with custom stoichiometry). Duplicates + are automatically combined. + propensity_type : Propensity + Propensity object defining the rate law (e.g., MassAction, Hill). + + Attributes + ---------- + inputs : list of WeightedSpecies + Reactant species with stoichiometry. + outputs : list of WeightedSpecies + Product species with stoichiometry. + propensity_type : Propensity + The rate law for this reaction. + is_reversible : bool + True if the propensity supports reversible kinetics. + species : list of Species + All species involved in the reaction (inputs, outputs, and + propensity species). + + See Also + -------- + Species : Chemical species in a CRN. + WeightedSpecies : Species with stoichiometric coefficient. + Propensity : Base class for rate laws. + MassAction : Mass action kinetics propensity. + + Notes + ----- A reaction has the form: - .. math:: \sum_i n_i I_i --> \sum_i m_i O_i @ rate = k + .. math:: + \sum_i n_i I_i \rightarrow \sum_i m_i O_i + + where :math:`n_i` is the stoichiometry of reactant :math:`I_i` and + :math:`m_i` is the stoichiometry of product :math:`O_i`. + + For reversible reactions: + + .. math:: + \sum_i n_i I_i \rightleftharpoons \sum_i m_i O_i + + Stoichiometry is handled as follows: + + - Species lists automatically combine duplicates + - A + A --> B becomes 2A --> B + - Stoichiometry affects rate calculations in mass action kinetics + + Different propensity types implement different rate laws: + + - MassAction: Standard mass action kinetics + - Hill functions: Cooperative binding kinetics + - GeneralPropensity: Custom formula + + Examples + -------- + Create a simple irreversible reaction: + + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> prop = bcp.MassAction(k_forward=0.1) + >>> rxn = bcp.Reaction([A], [B], prop) + + Create a reversible reaction: + + >>> C = bcp.Species('C') + >>> prop = bcp.MassAction(k_forward=100.0, k_reverse=0.01) + >>> rxn = bcp.Reaction([A, B], [C], prop) + >>> rxn.is_reversible + True + + Create a reaction with stoichiometry: + + >>> rxn = bcp.Reaction([A, A], [B], prop) # 2A <--> B - where n_i is the count of the ith input, I_i, and m_i is the count - of the ith output, O_i. If the reaction is reversible, the - reverse reaction is also included: + Use the from_massaction class method: - .. math:: \sum_i m_i O_i --> \sum_i n_i I_i @ rate = k_rev + >>> rxn = bcp.Reaction.from_massaction([A, B], [C], k_forward=100.0) """ @@ -42,13 +123,24 @@ def __init__( @property def propensity_type(self) -> Propensity: + """Propensity: The rate law for this reaction.""" return self._propensity_type @propensity_type.setter def propensity_type(self, new_propensity_type: Propensity): - """Replace the propensity type associated with the reaction object. + """Set the propensity type for the reaction. + + Parameters + ---------- + new_propensity_type : Propensity + New propensity object. Must be a valid Propensity subclass + instance. + + Raises + ------ + ValueError + If `new_propensity_type` is not a valid Propensity instance. - :param new_propensity_type: Valid propensity type """ if not Propensity.is_valid_propensity(new_propensity_type): raise ValueError( @@ -66,13 +158,41 @@ def from_massaction( k_forward: float, k_reverse: float = None, ): - """Initialize a Reaction object with mass action kinetics. + """Create a Reaction with mass action kinetics. + + Convenience constructor for creating reactions with MassAction + propensity. + + Parameters + ---------- + inputs : list of Species or list of WeightedSpecies + Reactant species. + outputs : list of Species or list of WeightedSpecies + Product species. + k_forward : float + Forward reaction rate constant. Must be positive. + k_reverse : float, optional + Reverse reaction rate constant. If None, reaction is + irreversible. If provided, must be positive. + + Returns + ------- + Reaction + New Reaction object with MassAction propensity. + + Examples + -------- + Create an irreversible reaction: + + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> rxn = bcp.Reaction.from_massaction([A], [B], k_forward=0.1) + + Create a reversible reaction: + + >>> rxn = bcp.Reaction.from_massaction( + ... [A, B], [C], k_forward=100.0, k_reverse=0.01) - :param inputs: - :param outputs: - :param k_forward: - :param k_reverse: - :return: Reaction object """ mak = MassAction(k_forward=k_forward, k_reverse=k_reverse) @@ -80,24 +200,51 @@ def from_massaction( @property def is_reversible(self) -> bool: + """bool: True if the reaction has reversible kinetics. + + Determined by the propensity type's is_reversible property. + + """ return self.propensity_type.is_reversible @property def inputs(self) -> List[WeightedSpecies]: + """List of WeightedSpecies: Reactant species with stoichiometry.""" return self._input_complexes @inputs.setter def inputs(self, new_input_complexes: List[WeightedSpecies]): + """Set the reaction inputs. + + Parameters + ---------- + new_input_complexes : list of Species or list of WeightedSpecies + New reactant species. Species are automatically converted to + WeightedSpecies. Duplicates are combined with adjusted + stoichiometry. + + """ self._input_complexes = Reaction._check_and_convert_complex_list( complexes=new_input_complexes ) @property def outputs(self) -> List[WeightedSpecies]: + """List of WeightedSpecies: Product species with stoichiometry.""" return self._output_complexes @outputs.setter def outputs(self, new_output_complexes: List[WeightedSpecies]): + """Set the reaction outputs. + + Parameters + ---------- + new_output_complexes : list of Species or list of WeightedSpecies + New product species. Species are automatically converted to + WeightedSpecies. Duplicates are combined with adjusted + stoichiometry. + + """ self._output_complexes = Reaction._check_and_convert_complex_list( complexes=new_output_complexes ) @@ -106,6 +253,37 @@ def outputs(self, new_output_complexes: List[WeightedSpecies]): def _check_and_convert_complex_list( complexes: Union[List[Species], List[WeightedSpecies]], ) -> List[WeightedSpecies]: + """Convert and validate species list to WeightedSpecies list. + + Converts Species to WeightedSpecies, validates all elements are + proper types, and combines duplicate species by summing + stoichiometry. + + Parameters + ---------- + complexes : list of Species or list of WeightedSpecies + Input species list to convert and validate. + + Returns + ------- + list of WeightedSpecies + Converted list with duplicates combined. Each unique species + appears once with total stoichiometry. + + Raises + ------ + TypeError + If `complexes` contains elements that are neither Species nor + WeightedSpecies. + + Notes + ----- + Duplicate handling examples: + + - [A, A, B] --> [2A, B] + - [WeightedSpecies(A, 2), A] --> [WeightedSpecies(A, 3)] + + """ if all( [isinstance(one_complex, Species) for one_complex in complexes] ): @@ -148,11 +326,37 @@ def _check_and_convert_complex_list( # return self.propensity_type.k_reverse def replace_species(self, species: Species, new_species: Species): - """Replaces species with new_species in the reaction. - - :param species: species to be replaced - :param new_species: the new species the old species is replaced with - :return: a new Reaction instance + """Create new reaction with a species replaced. + + Replaces all occurrences of a species throughout the reaction + (inputs, outputs, and propensity species) with a new species. + + Parameters + ---------- + species : Species + Species to be replaced. + new_species : Species + Species to replace with. + + Returns + ------- + Reaction + New Reaction object with species replaced. The original + reaction is not modified. + + Raises + ------ + ValueError + If either argument is not a Species object. + + Examples + -------- + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> C = bcp.Species('C') + >>> rxn = bcp.Reaction.from_massaction([A, B], [C], k_forward=0.1) + >>> D = bcp.Species('D') + >>> rxn2 = rxn.replace_species(A, D) # D + B --> C """ if not isinstance(species, Species) or not isinstance( @@ -193,7 +397,16 @@ def replace_species(self, species: Species, new_species: Species): return new_r def __repr__(self): - """Helper function to print the text of a rate function.""" + """Return string representation of the reaction. + + Returns + ------- + str + Reaction equation showing inputs, outputs, material types, and + attributes, but not rates or parameters. + + """ + # Helper function to print the text of a rate function. return self.pretty_print( show_rates=False, show_material=True, @@ -209,6 +422,44 @@ def pretty_print( show_parameters=True, **kwargs, ): + """Generate detailed, formatted string representation of reaction. + + Parameters + ---------- + show_rates : bool, default=True + If True, includes rate law formula below reaction equation. + show_material : bool, default=True + If True, shows species material types (e.g., 'dna', 'protein'). + show_attributes : bool, default=True + If True, shows species attributes. + show_parameters : bool, default=True + If True, shows parameter values in rate law. + **kwargs + Additional keyword arguments passed to species and propensity + pretty_print methods. Can include 'stochastic' (bool) for + stochastic vs deterministic rate display. + + Returns + ------- + str + Formatted reaction string. Format: + 'inputs --> outputs' or 'inputs <--> outputs' (reversible) + Optionally followed by rate law and parameters. + + Examples + -------- + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> rxn = bcp.Reaction.from_massaction([A], [B], k_forward=0.1) + >>> print(rxn.pretty_print()) + A --> B + Kf=k_forward * A + k_forward=0.1 Kf = 0.1 * A + + >>> print(rxn.pretty_print(show_rates=False)) + A --> B + + """ kwargs['show_rates'] = show_rates kwargs['show_material'] = show_material kwargs['show_attributes'] = show_attributes @@ -232,12 +483,39 @@ def pretty_print( return txt def __eq__(self, other): - """Check if reactions are equivalent. + """Test equality between reactions. + + Two reactions are equal if they have the same inputs, outputs, + and propensity (in any order). + + Parameters + ---------- + other : Reaction + Another reaction to compare with. + + Returns + ------- + bool + True if reactions have identical inputs, outputs, and + propensity. - Two reactions are equivalent if they have the same inputs, - outputs, and propensity. + Raises + ------ + TypeError + If `other` is not a Reaction object. + + Notes + ----- + Order of species in inputs/outputs doesn't matter: + + - A + B --> C equals B + A --> C + - Species are compared using sets """ + # Check if reactions are equivalent. + # + # Two reactions are equivalent if they have the same inputs, + # outputs, and propensity. if not isinstance(other, Reaction): raise TypeError( f"Only reactions can be compared with reaction! " @@ -256,14 +534,37 @@ def __eq__(self, other): ) == (set(other.inputs), set(other.outputs), other.propensity_type) def __contains__(self, item: Species): - """It checks whether a species is part of a reaction. - - Checks the input and output lists as well as the propensity - type for the species. - - :param item: a Species instance - :return: bool - :exception NotImplementedError for non-Species objects + """Check if a species is involved in the reaction. + + Checks whether a species appears in the reaction's inputs, + outputs, or propensity (e.g., Hill kinetics with regulatory + species). + + Parameters + ---------- + item : Species + Species to check for. + + Returns + ------- + bool + True if species is in inputs, outputs, or propensity species. + + Raises + ------ + NotImplementedError + If `item` is not a Species object. + + Examples + -------- + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> C = bcp.Species('C') + >>> rxn = bcp.Reaction.from_massaction([A], [B], k_forward=0.1) + >>> A in rxn + True + >>> C in rxn + False """ if isinstance(item, Species): @@ -279,12 +580,32 @@ def __contains__(self, item: Species): @property def species(self) -> List[Species]: - """List of species in the reactions. - - Returns a list of species in the reactions collected from the inputs - and outputs and the propensity (e.g. Hill kinetics has species in it). - - :return: list of species in the reactions + """List of Species: All species involved in the reaction. + + Returns a flattened list of all species from inputs, outputs, and + the propensity (e.g., Hill functions have regulatory species). + + Returns + ------- + list of Species + All species in the reaction. May contain duplicates if a + species appears in multiple roles. + + Notes + ----- + This property collects species from three sources: + + 1. Input species (reactants) + 2. Output species (products) + 3. Propensity species (e.g., activators/repressors in Hill) + + Examples + -------- + >>> A = bcp.Species('A') + >>> B = bcp.Species('B') + >>> rxn = bcp.Reaction.from_massaction([A], [B], k_forward=0.1) + >>> rxn.species + [A, B] """ in_part = [] diff --git a/biocrnpyler/core/species.py b/biocrnpyler/core/species.py index f21e12b9..4dd32847 100644 --- a/biocrnpyler/core/species.py +++ b/biocrnpyler/core/species.py @@ -12,8 +12,95 @@ class Species(OrderedMonomer): """A formal species object for a chemical reaction network (CRN). - A Species must have a name. They may also have a material_type (such as - DNA, RNA, Protein), and a list of attributes. + Represents a chemical species in a CRN with a name, material type, + attributes, and compartment. Species inherits from `OrderedMonomer`, + allowing it to be part of polymer structures while also functioning as + an independent chemical entity in reactions. + + Parameters + ---------- + name : str + Name of the species. Must consist of letters, numbers, or + underscores, cannot contain double underscores, and cannot + begin/end with special characters. + material_type : str, default='' + Type of material (e.g., 'dna', 'rna', 'protein', 'complex'). + Required if name starts with a number. Must start with a letter. + attributes : list of str or None, optional + List of attribute tags for the species (e.g., 'degraded', + 'phosphorylated'). Each attribute must be alphanumeric. + compartment : Compartment, str, or None, optional + The compartment containing this species. If None, uses default + compartment. If str, creates a new Compartment with that name. + **kwargs + Additional keyword arguments passed to `OrderedMonomer`. + + Attributes + ---------- + name : str + The name of the species. + material_type : str + The material type of the species. + attributes : list of str + List of attribute tags associated with the species. + compartment : Compartment + The compartment containing this species. + direction : str, int, or None + Directional orientation (inherited from `OrderedMonomer`). When + set, the direction is also added as an attribute. + + See Also + -------- + ComplexSpecies : Species formed from multiple bound species. + OrderedPolymerSpecies : Polymer species for chemical reactions. + WeightedSpecies : Species with stoichiometry coefficient. + + Notes + ----- + Species names must: + + - Contain only letters, numbers, and underscores + - Not contain double underscores ('__') + - Not end with an underscore + - Start with a letter or number (if starting with number, requires + material_type) + + Species are represented as strings in the format: + + `material_type_name_attribute1_attribute2_compartment` + + Components are omitted if empty or default values. + + Two species + are equal if they have the same name, material_type, attributes, + compartment, parent, and position. + + Examples + -------- + Create a simple species: + + >>> S = bcp.Species('S') + >>> S.name + 'S' + + Create a protein with attributes: + + >>> GFP = bcp.Species( + ... name='GFP', + ... material_type='protein', + ... attributes=['fluorescent', 'degraded'] + ... ) + >>> repr(GFP) + 'protein_GFP_fluorescent_degraded' + + Create a species in a compartment: + + >>> cytoplasm = bcp.Compartment('cytoplasm') + >>> enzyme = bcp.Species( + ... name='enzyme', + ... material_type='protein', + ... compartment=cytoplasm + ... ) """ @@ -52,7 +139,20 @@ def attributes(self, attributes): self._attributes = [] def remove_attribute(self, attribute: str): - """Remove an attribute from a Species.""" + """Remove an attribute from the species. + + Parameters + ---------- + attribute : str + The attribute to remove. Must be an alphanumeric string. + + Notes + ----- + If the attribute is not present or is None, no action is taken. + All occurrences of the attribute are removed from the attributes + list. + + """ if not hasattr(self, '_attributes') or attribute is None: return else: @@ -62,7 +162,32 @@ def remove_attribute(self, attribute: str): self._attributes = [a for a in self.attributes if a != attribute] def add_attribute(self, attribute: str): - """Adds attribute to a Species.""" + """Add an attribute to the species. + + Parameters + ---------- + attribute : str + The attribute to add. Must be an alphanumeric string and + non-None. + + Raises + ------ + AssertionError + If attribute is not an alphanumeric string or is None. + + Notes + ----- + Duplicate attributes are not added - each attribute appears only + once in the attributes list. + + Examples + -------- + >>> species = bcp.Species('MyProtein') + >>> species.add_attribute('degraded') + >>> species.attributes + ['degraded'] + + """ if not hasattr(self, '_attributes'): self._attributes = [] assert ( @@ -112,10 +237,22 @@ def direction(self): @direction.setter def direction(self, direction): - """Direction attribute. - - This is inheritted from OrderedMonomer. A species with direction - will use it as an attribute as well. This is overwritten to make + """Set the directional orientation of the species. + + Overrides `OrderedMonomer.direction` to automatically add the + direction as an attribute and remove the old direction attribute. + + Parameters + ---------- + direction : str, int, or None + The direction to assign. Common values include 'forward', + 'reverse', 0, 1, or None. When set, the direction is added as + an attribute. + + Notes + ----- + This is inherited from `OrderedMonomer`. A species with direction + will use it as an attribute as well. This is overwritten to make direction an attribute. """ @@ -128,7 +265,17 @@ def direction(self, direction): self.add_attribute(direction) def remove(self): - """Remove direction as an attribute.""" + """Remove the species from its parent polymer. + + Overrides `OrderedMonomer.remove` to also remove the direction + attribute if present. + + Returns + ------- + Species + Returns self after removal for method chaining. + + """ if self.direction is not None: self.remove_attribute(self.direction) return OrderedMonomer.remove(self) # call the OrderedMonomer function @@ -136,11 +283,33 @@ def remove(self): # Note: this is used because properties can't be overwritten without # setters being overwritten in subclasses. def _check_name(self, name): - """Check that name is in proper format. - - Check that the string contains only underscores and alpha-numeric - characters or is None. Additionally cannot end in "_" or contain - double "__", also cannot start with a number. + """Validate that name follows proper formatting rules. + + Parameters + ---------- + name : str or None + The name to validate. + + Returns + ------- + str or None + The validated name, unchanged if valid. + + Raises + ------ + ValueError + If name violates formatting rules. + TypeError + If name is not a string or None. + + Notes + ----- + Valid names must: + + - Contain only underscores and alphanumeric characters + - Not contain double underscores ('__') + - Not end with an underscore + - Start with an alphanumeric character """ if name is None: @@ -207,7 +376,10 @@ def __repr__(self): for i in self.attributes: if i is not None: txt += '_' + str(i) - if self.compartment.name != 'default': + if ( + self.compartment is not None + and self.compartment.name != 'default' + ): # Only add a compartment name if it is not the default one. if # compartment name is already there with an underscore remove it # from the string first to not repeat the compartment tag @@ -217,6 +389,30 @@ def __repr__(self): return txt def replace_species(self, species, new_species): + """Replace a species with another species. + + For a simple Species, returns `new_species` if this species equals + `species`, otherwise returns self. For complex species, acts + recursively. + + Parameters + ---------- + species : Species + The species to search for and replace. + new_species : Species + The species to replace with. + + Returns + ------- + Species + Either `new_species` (if self == species) or self. + + Raises + ------ + ValueError + If either argument is not a Species instance. + + """ if not isinstance(species, Species): raise ValueError( "species argument must be an instance of Species!" @@ -233,10 +429,19 @@ def replace_species(self, species, new_species): return self def get_species(self, recursive=None): - """Get species list. + """Get a list containing this species. + + Returns + ------- + list of Species + A list containing only this species: [self]. - Used in some recursive calls where ComplexSpecies returns a list - and Species will return just themselves (in a list). + Notes + ----- + This method is used in recursive calls where `ComplexSpecies` + returns a list of constituent species while `Species` returns just + itself in a list. The `recursive` parameter is accepted for + compatibility but not used in the base Species class. """ return [self] @@ -249,12 +454,40 @@ def pretty_print( show_initial_condition=False, **kwargs, # TODO: allows spurious keywords; fix... ): - """A more powerful printing function. - - Useful for understanding CRNs but does not return string - identifiers. show_material toggles whether species.material is - printed. show_attributes toggles whether species.attributes is - printed + """Generate a human-readable string representation of the species. + + Parameters + ---------- + show_material : bool, default=True + If True, includes material_type in brackets around the species. + show_compartment : bool, default=False + If True, shows the compartment name in the representation. + show_attributes : bool, default=True + If True, includes attributes in parentheses after the name. + show_initial_condition : bool, default=False + Placeholder for compatibility with CRN printing. + **kwargs + Additional keyword arguments (currently unused). + + Returns + ------- + str + Formatted string representation of the species. + + Notes + ----- + This method provides more detailed output than `__repr__`, + useful for understanding CRNs but does not return string + identifiers compatible with parsers. + + Format: `material_type[name(attr1, attr2)-direction]` + + Examples + -------- + >>> S = bcp.Species('S', material_type='protein', + ... attributes=['active']) + >>> S.pretty_print() + 'protein[S(active)]' """ txt = '' @@ -288,16 +521,25 @@ def pretty_print( def __eq__(self, other): """Check if two species are equivalent. - Overrides the default implementation. Two species are equivalent - if they have the same name, type, and attributes. + Two species are equal if they have the same name, material_type, + attributes (as sets), parent, compartment, and position. + + Parameters + ---------- + other : Species + The species to compare with. - :param other: Species instance + Returns + ------- + bool + True if species are equivalent, False otherwise. - :return: boolean + Notes + ----- + Equality between parents and children can result in loops, so + string equality is used for parent comparison. """ - # Note: "==" equality between parents and children can result in - # loops, so string equality is used if ( isinstance(other, Species) and self.material_type == other.material_type @@ -312,10 +554,24 @@ def __eq__(self, other): return False def monomer_eq(self, other): - """Check if two monomers are equal. + """Check if two monomers are equal, ignoring parent and position. - Same as normal equality, but does not check for parents or - positions. + Parameters + ---------- + other : Species + The species to compare with. + + Returns + ------- + bool + True if species have the same name, material_type, attributes, + and compartment, regardless of parent or position. + + Notes + ----- + This is the same as normal equality but does not check for parents + or positions. Useful for comparing species that may be in different + polymer contexts. """ if ( @@ -342,10 +598,25 @@ def __contains__(self, item): return item in self.get_species() def contains_species_monomer(self, s): - """Checks if the Species has a monomer (Species) inside of it. - - Checks without checking Species.parent, Species.position, or - direction. In effect, a less stringent version of __contains__. + """Check if the species contains a monomer, ignoring context. + + Parameters + ---------- + s : Species + The species monomer to search for. + + Returns + ------- + bool + True if the species contains a monomer equal to `s` (ignoring + parent, position, and direction), False otherwise. + + Notes + ----- + This is a less stringent version of `__contains__` that checks + without considering Species.parent, Species.position, or direction. + Useful for determining if a species is present regardless of its + polymer context. """ s_copy = copy.deepcopy(s) @@ -359,7 +630,28 @@ def contains_species_monomer(self, s): @staticmethod def flatten_list(in_list) -> List: - """Helper function to flatten lists.""" + """Recursively flatten a nested list of species. + + Parameters + ---------- + in_list : list or Species + A potentially nested list of species, or a single species. + + Returns + ------- + list + Flattened list containing all species. None elements are + filtered out. + + Examples + -------- + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> nested = [S1, [S2, None]] + >>> bcp.Species.flatten_list(nested) + [S1, S2] + + """ out_list = [] if not isinstance(in_list, list): out_list.append(in_list) @@ -375,8 +667,43 @@ def flatten_list(in_list) -> List: class WeightedSpecies: + """Container for a species with stoichiometric coefficient. + + Wraps a `Species` object together with its stoichiometry for use in + reactions. This class is primarily used internally by the Reaction + class to represent reactants and products with their coefficients. + + Parameters + ---------- + species : Species + The species object. + stoichiometry : int, default=1 + The stoichiometric coefficient. Must be a positive integer. + + Attributes + ---------- + species : Species + The wrapped species object. + stoichiometry : int + The stoichiometric coefficient (positive integer). + + See Also + -------- + Species : Base class for chemical species. + Reaction : Chemical reaction containing weighted species. + + Examples + -------- + Create a weighted species: + + >>> S = bcp.Species('S') + >>> ws = bcp.WeightedSpecies(species=S, stoichiometry=2) + >>> ws.stoichiometry + 2 + + """ + def __init__(self, species: Species, stoichiometry: int = 1): - """Container object for all types of species and its stoichiometry.""" self.species: Species = species self.stoichiometry: int = stoichiometry @@ -404,19 +731,32 @@ def replace_species(self, *args, **kwargs): @staticmethod def _count_weighted_species(weighted_species: List): - """Merge the same species in a list with different stoichiometry. - - >>> s1 = Species(name='a') - >>> ws1 = WeightedSpecies(species=s1, stoichiometry=2) - >>> ws2 = WeightedSpecies(species=s1, stoichiometry=5) + """Merge species in a list with different stoichiometry. + + Combines `WeightedSpecies` objects with the same species by summing + their stoichiometric coefficients. + + Parameters + ---------- + weighted_species : list of WeightedSpecies + List of weighted species to merge. + + Returns + ------- + dict + Dictionary mapping unique `WeightedSpecies` to their total + stoichiometry. + + Examples + -------- + >>> s1 = bcp.Species(name='a') + >>> ws1 = bcp.WeightedSpecies(species=s1, stoichiometry=2) + >>> ws2 = bcp.WeightedSpecies(species=s1, stoichiometry=5) >>> ws_list = [ws1, ws2] - >>> freq_dict = WeightedSpecies._count_weighted_species(ws_list) + >>> freq_dict = bcp.WeightedSpecies._count_weighted_species(ws_list) >>> len(freq_dict) 1 - :param weighted_species: list of weighted_species - :return: unique list of weighted_species, i.e. set(weighted_species) - """ # convert to set doesn't work because we need only species equality unique_species = [] @@ -447,18 +787,78 @@ def __hash__(self): class ComplexSpecies(Species): - """Internal representation of a complex species. - - ComplexSpecies and OrderedComplexSpecies should ALWAYS be created with - the Complex function. - - A special kind of species which is formed as a complex of two or more - species. Used for attribute inheritance and storing groups of bounds - Species. Note that in a ComplexSpecies, the order of the species list - does not matter. This means that ComplexSpecies([s1, s2]) = - ComplexSpecies([s2, s1]). This is good for modelling order-indpendent - binding complexes. For a case where species order matters - (e.g. polymers) use OrderedComplexSpecies + """Species formed from multiple bound species. + + A special kind of species representing a complex of two or more bound + species. ComplexSpecies should always be created using the `Complex` + function, not directly. Order of species in the list does not matter: + ComplexSpecies([s1, s2]) == ComplexSpecies([s2, s1]). + + Parameters + ---------- + species : list of Species or str + List of species forming the complex. Must contain at least 2 + species. + name : str or None, optional + Custom name for the complex. If None, generates a name from + constituent species. + material_type : str, default='complex' + Material type identifier for the complex. + attributes : list of str or None, optional + Attributes for the complex species. + compartment : Compartment, str, or None, optional + Compartment containing the complex. + called_from_complex : bool, default=False + Internal flag to enforce use of `Complex` function. + + Attributes + ---------- + species : list of Species + Sorted list of constituent species in the complex. + species_set : list of Species + Unique species in the complex, sorted by string representation. + name : str + Name of the complex (auto-generated if not provided). + + See Also + -------- + Complex : Metaclass for creating ComplexSpecies. + OrderedComplexSpecies : Complex where species order matters. + Species : Base class for chemical species. + + Notes + ----- + ComplexSpecies add an additional '_' at the end of their string + representation to differentiate edge cases. + + Species order does not affect equality: ComplexSpecies([s1, s2]) + == ComplexSpecies([s2, s1]) + + For ordered complexes, use `OrderedComplexSpecies`. + + If no name is provided, the complex is named by concatenating all + constituent species names with counts for duplicates. + + Always use the `Complex` function to create `ComplexSpecies`: + + >>> # Correct + >>> complex_species = bcp.Complex([S1, S2]) + + >>> # Incorrect (will raise warning) + >>> complex_species = bcp.ComplexSpecies([S1, S2]) + + Examples + -------- + Create a complex (using Complex function): + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> complex_species = bcp.Complex([S1, S2]) + + Check if a species is in a complex: + + >>> S1 in complex_species + True """ @@ -496,10 +896,18 @@ def __init__( ) def __repr__(self): - """String representation of ComplexSpecies. - - ComplexSpecies add an additional "_" onto the end of their string - representation. This ensures that some edge cases are + """Generate string representation of ComplexSpecies. + + Returns + ------- + str + String representation with an additional '_' at the end to + differentiate edge cases. + + Notes + ----- + ComplexSpecies add an additional '_' onto the end of their string + representation. This ensures that some edge cases are differentiated. """ @@ -526,7 +934,30 @@ def name(self, name: str): self._name = self._check_name(name) def __contains__(self, item): - """Returns a list of species inside the ComplexSpecies.""" + """Check if a species is contained in the complex. + + Parameters + ---------- + item : Species + The species to search for. + + Returns + ------- + bool + True if the species is found in the complex or any nested + complexes, False otherwise. + + Raises + ------ + ValueError + If `item` is not a Species instance. + + Notes + ----- + This method searches recursively through nested ComplexSpecies to + find the target species. + + """ if not isinstance(item, Species): raise ValueError( "Operator 'in' requires chemical_reaction_network.Species " @@ -570,10 +1001,27 @@ def species(self, species): self._species = species def replace_species(self, species: Species, new_species: Species): - """Replace species with new_species in the entire Complex Species. + """Replace a species throughout the entire complex. + + Acts recursively on nested ComplexSpecies. Does not modify in + place - returns a new ComplexSpecies. + + Parameters + ---------- + species : Species + The species to replace. + new_species : Species + The species to replace with. - Acts recursively on nested ComplexSpecies Does not act in place - - returns a new ComplexSpecies. + Returns + ------- + ComplexSpecies + A new ComplexSpecies with the replacement applied. + + Raises + ------ + ValueError + If either argument is not a Species instance. """ if not isinstance(species, Species): @@ -603,10 +1051,19 @@ def replace_species(self, species: Species, new_species: Species): ) def get_species(self, recursive=False): - """Returns all species in the ComplexSpecies. + """Get all species in the complex. + + Parameters + ---------- + recursive : bool, default=False + If True, returns species inside nested ComplexSpecies + recursively. If False, returns only this ComplexSpecies. - If recursive = True, returns species inside internal ComplexSpecies - recursively as well. + Returns + ------- + list of Species + List of species. If recursive=False, returns [self]. If + recursive=True, returns [self] plus all constituent species. """ if not recursive: @@ -671,21 +1128,92 @@ def pretty_print( return txt def monomer_count(self, m): - """Effectively self.species.count(m) using monomer_eq for equality.""" - return sum([s.monomer_eq(m) for s in self.species]) + """Count occurrences of a monomer in the complex. + Parameters + ---------- + m : Species + The monomer to count. -class OrderedComplexSpecies(ComplexSpecies): - """Create an ordered complex species. + Returns + ------- + int + Number of times the monomer appears in the complex, using + `monomer_eq` for equality comparison. - ComplexSpecies and OrderedComplexSpecies should ALWAYS be created with - the Complex function. + Notes + ----- + This effectively implements `self.species.count(m)` but uses + `monomer_eq` for equality, which ignores parent and position. + + """ + return sum([s.monomer_eq(m) for s in self.species]) - A special kind of species which is formed as a complex of two or more - species. In OrderedComplexSpecies the order in which the complex - subspecies are is defined denote different species, eg [s1, s2, s3] != - [s1, s3, s2]. Used for attribute inheritance and storing groups of - bounds Species. + +class OrderedComplexSpecies(ComplexSpecies): + """Complex species where species order is significant. + + A special kind of species formed from a complex of two or more species + where the order matters. OrderedComplexSpecies should always be created + using the `Complex` function with `ordered=True`, not directly. + Unlike ComplexSpecies, [s1, s2, s3] != [s1, s3, s2]. + + Parameters + ---------- + species : list of Species or str + Ordered list of species forming the complex. Must contain at least + 2 species. + name : str or None, optional + Custom name for the complex. If None, generates a name from + constituent species in order. + material_type : str, default='ordered_complex' + Material type identifier for the ordered complex. + attributes : list of str or None, optional + Attributes for the complex species. + compartment : Compartment, str, or None, optional + Compartment containing the complex. + called_from_complex : bool, default=False + Internal flag to enforce use of `Complex` function. + + Attributes + ---------- + species : list of Species + Ordered list of constituent species (NOT sorted). + name : str + Name of the complex (auto-generated if not provided). + + See Also + -------- + Complex : Metaclass for creating ordered complexes. + ComplexSpecies : Complex where species order doesn't matter. + OrderedPolymerSpecies : Ordered polymer for chemical reactions. + + Notes + ----- + Unlike `ComplexSpecies`, the order of species matters: + OrderedComplexSpecies([s1, s2]) != OrderedComplexSpecies([s2, s1]) + + Similar to ComplexSpecies, OrderedComplexSpecies add an additional + '_' at the end. + + Always use `Complex` with `ordered=True`: + + >>> # Correct + >>> complex_species = bcp.Complex([S1, S2], ordered=True) + + >>> # Incorrect (will raise warning) + >>> complex_species = bcp.OrderedComplexSpecies([S1, S2]) + + Examples + -------- + Create an ordered complex: + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> ordered = bcp.Complex([S1, S2], ordered=True) + >>> reversed = bcp.Complex([S2, S1], ordered=True) + >>> ordered == reversed + False """ @@ -844,23 +1372,80 @@ def pretty_print( class OrderedPolymerSpecies(OrderedComplexSpecies, OrderedPolymer): - """OrderedPolymers which can also participate in chemical reactions. - - OrderedPolymerSpecies is made up of Species (which are also - OrderedMonomers). - - The Species inside an OrderedPolymerSpecies are meant to model multiple - binding sites and/or functional regions. ComplexSpecies can be formed - inside an OrderedPolymer by passing the internal Species at a specific - location. - - When used as an input to a reaction, OrderedPolymerSpecies can be - passed or one if its internal Species (eg a Species with Species.parent - = OrderedPolymerSpecies) can also be used to produce the same - reaction. This allows flexibility in the arguments to different - Mechanisms. Sometimes, it is convenient to pass in the - OrderedPolymerSpecies, sometimes it is convenient to pass an internal - Species. Both will work from the point of view of any Mechanism. + """Ordered polymer that can participate in chemical reactions. + + A polymer composed of Species (which are also OrderedMonomers) that can + act as a reactant or product in chemical reactions. The internal + species represent multiple binding sites and/or functional regions. + + Parameters + ---------- + species : list of Species or list of [Species, direction] + List of species monomers to form the polymer. Each element can be + a Species or a [Species, direction] pair. + name : str or None, optional + Custom name for the polymer. If None, auto-generated from + constituent species. + material_type : str, default='ordered_polymer' + Material type identifier for the polymer. + compartment : Compartment, str, or None, optional + Compartment containing the polymer. + attributes : list of str or None, optional + Attributes for the polymer species. + circular : bool, default=False + If True, the polymer has circular topology (e.g., plasmid). + + Attributes + ---------- + polymer : tuple of Species + Ordered tuple of species monomers in the polymer. + species : tuple of Species + Alias for `polymer` (inherited from OrderedPolymer). + circular : bool + Flag indicating circular topology. + default_material : str + Class attribute defining default material type. + + See Also + -------- + OrderedPolymer : Base class for ordered polymers. + OrderedComplexSpecies : Ordered complex base class. + PolymerConformation : Set of polymers with connections. + + Notes + ----- + When used as a reaction input, either the entire + OrderedPolymerSpecies or one of its internal Species (with + Species.parent = OrderedPolymerSpecies) can be passed to mechanisms. + + Species inside an `OrderedPolymerSpecies` model multiple binding + sites and/or functional regions. `ComplexSpecies` can be formed at + specific locations by passing the internal Species. + + The `circular` attribute indicates circular topology but does not + automatically enforce circular constraints in operations. + + Examples + -------- + Create a linear polymer: + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> polymer = bcp.OrderedPolymerSpecies( + ... species=[S1, S2], + ... name='my_polymer' + ... ) + >>> len(polymer) + 2 + + Create a circular polymer (plasmid): + + >>> plasmid = bcp.OrderedPolymerSpecies( + ... species=[S1, S2], + ... circular=True + ... ) + >>> plasmid.circular + True """ @@ -922,10 +1507,39 @@ def __init__( @classmethod def from_polymer_species(cls, ops, replace_dict, **kwargs): - """OrderedPolymerSpecies with certain monomers replaced. - - inputs: replace_dict {monomer index --> new Species} - outputs: OrderedPolymerSpecies + """Create OrderedPolymerSpecies with specific monomers replaced. + + Parameters + ---------- + ops : OrderedPolymerSpecies + The original polymer species to modify. + replace_dict : dict + Dictionary mapping monomer indices (int) to new Species to + insert at those positions. + **kwargs + Additional keyword arguments for the new OrderedPolymerSpecies. + Defaults are inherited from `ops` if not specified. + + Returns + ------- + OrderedPolymerSpecies + New polymer with specified monomers replaced. + + Notes + ----- + If `replace_dict` is empty, returns a deep copy of `ops`. + + Examples + -------- + Replace monomer at position 1: + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> S3 = bcp.Species('S3') + >>> polymer = bcp.OrderedPolymerSpecies([S1, S2]) + >>> new_polymer = bcp.OrderedPolymerSpecies.from_polymer_species( + ... polymer, {1: S3} + ... ) """ if replace_dict == {}: @@ -1034,33 +1648,70 @@ def __contains__(self, item): class PolymerConformation(Species, MonomerCollection): - """Set of polymers and connections in the form of ComplexSpecies. - - This class stores a set of PolymerSpecies and a set of connections - between them in the form of ComplexSpecies containing Monomers inside - the PolymerSpecies. - - The main function of this class is to provide a unique name to each - conformation. The name is given by: - - conformation__[PolymerSpecies 1]_..._[PolymerSpecies N] - _[ComplexSpecies_1 parent Polymer indices] - _[ComplexSpecies_1]..._[ComplexSpecies_M]__ - - where the list of PolymerSpecies and ComplexSpecies are in alphabetical - order. The ComplexSpecies parent Polymer indices notes which Polymers - each Species in the ComplexSpecies comes from, with 'n' used for None. - - In general, users should not produce PolymerConformations directly. The - Complex function will automatically produce these when a complex is - formed involving Multiple OrderedMonomers contained within one or more - PolymerSpecies. - - In effect, this can be thought of as a data structure for a - hypergraph. The monomers of the PolymerSpecies are vertices and - ComplexSpecies form edges that connect an arbitrary number of vertices - (potentially including other Species as well). Note that this class - allows for multiple edges between the same sets of vertices. + """Set of polymers and their connections via ComplexSpecies. + + Represents a conformation of one or more PolymerSpecies connected by + ComplexSpecies containing monomers from the polymers. This class + provides unique naming for conformations and serves as a data structure + for polymer hypergraphs. + + Parameters + ---------- + complexes : list of ComplexSpecies, optional + List of ComplexSpecies connecting monomers from + OrderedPolymerSpecies. Must contain monomers from the polymers. + polymer : OrderedPolymerSpecies or list of Species, optional + Single polymer or list of species to form a polymer. Exactly one + of `complexes` or `polymer` must be provided. + material_type : str, default='conformation' + Material type identifier. + name : str or None, optional + Custom name for the conformation. If None, auto-generated. + **kwargs + Additional keyword arguments passed to Species constructor. + + Attributes + ---------- + polymers : list of OrderedPolymerSpecies + List of polymers in this conformation. + complexes : list of ComplexSpecies + List of complexes connecting monomers in the polymers. + name : str + Auto-generated name encoding polymer and complex structure. + + See Also + -------- + OrderedPolymerSpecies : Polymer species for chemical reactions. + ComplexSpecies : Complex of multiple bound species. + Complex : Metaclass for creating complexes. + + Notes + ----- + Auto-generated names follow the format: + `conformation__[Polymer1]_[Polymer2]_[indices]_[Complex1]_[Complex2]__` + + where indices encode which polymers each complex binds to and the list of + `PolymerSpecies` and `ComplexSpecies` are in alphabetical order. + + A `PolymerConformation` represents a hypergraph where: + + - Monomers are vertices + - `ComplexSpecies` are hyperedges connecting arbitrary numbers of + vertices + - Multiple edges between the same vertices are allowed + + Users typically do not create PolymerConformations directly. The + `Complex` function automatically creates them when complexing + monomers from OrderedPolymerSpecies. + + Examples + -------- + Create from a single polymer: + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> polymer = bcp.OrderedPolymerSpecies([S1, S2]) + >>> conformation = bcp.PolymerConformation(polymer=polymer) """ @@ -1072,12 +1723,6 @@ def __init__( name=None, **kwargs, ): - """Initialize PolymerConformation class. - - complexes: a list of ComplexSpecies each of which must contain - Monomers from the OrderedPolymerSpecies in the conformation - - """ Species.__init__( self, name=name, material_type=material_type, **kwargs ) @@ -1115,13 +1760,33 @@ def __init__( def from_polymer_conformation( cls, pcs, complexes=None, complexes_to_remove=None, **kwargs ): - """New PolymerConformation from prevous conformaiton plus complexes. - - This function produces a new PolymerConformation from previously - existing PolymerConformations and new Complexes. - - pcs: a list of PolymerConformations - complexes: a list of complexes to add to the polymer conformation + """Create PolymerConformation from existing conformations. + + Produces a new PolymerConformation by merging complexes from + previous PolymerConformations and adding new complexes. + + Parameters + ---------- + pcs : list of PolymerConformation + List of existing PolymerConformations to merge. + complexes : list of ComplexSpecies, optional + Additional complexes to add to the conformation. Default is an + empty list. + complexes_to_remove : list of ComplexSpecies, optional + Complexes to exclude from the merged conformation. Default is + an empty list. + **kwargs + Additional keyword arguments for the new PolymerConformation. + + Returns + ------- + PolymerConformation + New conformation merging all input conformations and complexes. + + Raises + ------ + TypeError + If `pcs` is not a list of PolymerConformations. """ if not isinstance(pcs, list) or not any( @@ -1148,17 +1813,43 @@ def from_polymer_conformation( def from_polymer_replacement( cls, pc, old_polymers, new_polymers, **kwargs ): - """Replace old polymers with new polymers. - - This function produces a PolymerConformation from a previously - existing PolymerConformation by replacing old_polymers with - new_polymers - - pc: the PolymerConformation to replace polymers from. - old_polymers: a list of PolymerSpecies instances. These must be - the same instances stored inside pc or an error is thrown. - new_polymers: a list of new PolymerSpecies instances to replace - each of the old_polymers. Must be the same length as old_polymers. + """Create PolymerConformation by replacing polymers. + + Produces a PolymerConformation from an existing one by replacing + specified polymers with new ones, updating all complexes + accordingly. + + Parameters + ---------- + pc : PolymerConformation + The conformation to modify. + old_polymers : list of OrderedPolymerSpecies + Polymers to replace. Must be the same instances (not just + equal) as those in `pc.polymers`. + new_polymers : list of OrderedPolymerSpecies + New polymers to use as replacements. Must be the same length + as `old_polymers`. + **kwargs + Additional keyword arguments for the new PolymerConformation. + Defaults are inherited from `pc` if not specified. + + Returns + ------- + PolymerConformation + New conformation with polymers replaced. + + Raises + ------ + TypeError + If arguments are not the correct types. + ValueError + If `old_polymers` are not instances in `pc.polymers`, or if + lists have different lengths. + + Notes + ----- + This method updates all complexes to reference monomers from the + new polymers at the same positions as in the old polymers. """ if not isinstance(pc, PolymerConformation): @@ -1537,49 +2228,125 @@ def __repr__(self): class Complex: """Metaclass for creating chemical complexes. - Complex is not a class that gets instantiated - it creates - ComplexSpecies and OrderedComplexSpecies. The Logic encoded in - the __new__ function is used to insert these classes into the - binding sites of OrderedPolymerSpecies. + `Complex` is not a class that gets instantiated directly - it creates + instances of `ComplexSpecies`, `OrderedComplexSpecies`, + `OrderedPolymerSpecies`, or `PolymerConformation` based on the input + species and their parent relationships. - Arguments: - species: a list of species to put into ComplexSpecies or - OrderedComplexSpecies - - kwargs: - ordered: whether to produce an OrderedComplexSpecies (default = False) + Parameters + ---------- + species : list of Species + List of species to combine into a complex. Can include standalone + Species, Species with parents (monomers in polymers), or entire + OrderedPolymerSpecies. + ordered : bool, default=False + If True, creates OrderedComplexSpecies where species order + matters. If False, creates ComplexSpecies where order is + irrelevant. + **kwargs + Additional keyword arguments passed to the created species class. + + Returns + ------- + ComplexSpecies, OrderedComplexSpecies, OrderedPolymerSpecies, or + PolymerConformation + The type of species returned depends on the input structure: + + - Simple species list -> ComplexSpecies or OrderedComplexSpecies + - Monomers from one polymer -> OrderedPolymerSpecies + - Monomers from multiple polymers/conformations -> + PolymerConformation + + See Also + -------- + ComplexSpecies : Unordered complex of multiple species. + OrderedComplexSpecies : Ordered complex of multiple species. + OrderedPolymerSpecies : Polymer species for reactions. + PolymerConformation : Multiple polymers with connections. + + Notes + ----- + The `__new__` method implements logic for different scenarios: + + 1. No parents: Creates ComplexSpecies or OrderedComplexSpecies + 2. Single polymer parent: Creates OrderedPolymerSpecies with + complex at binding site + 3. Multiple polymer parents or conformations: Creates + PolymerConformation merging all complexes + 4. Error cases: Raises exceptions for invalid combinations + + The correct species type is automatically determined from the input, + allowing flexible complex formation without explicit type selection. + + Examples + -------- + Create a simple complex: + + >>> S1 = bcp.Species('S1') + >>> S2 = bcp.Species('S2') + >>> complex = bcp.Complex([S1, S2]) + >>> type(complex) + biocrnpyler.core.species.ComplexSpecies + + Create an ordered complex: + + >>> ordered = bcp.Complex([S1, S2], ordered=True) + >>> type(ordered) + biocrnpyler.core.species.OrderedComplexSpecies + + Create a complex at a polymer binding site: + + >>> S3 = bcp.Species('S3') + >>> polymer = bcp.OrderedPolymerSpecies([S1, S2]) + >>> # S1 is now inside the polymer at position 0 + >>> complex = bcp.Complex([polymer[0], S3]) + >>> type(complex.parent) + biocrnpyler.core.species.OrderedPolymerSpecies """ def __new__(cls, *args, **kwargs): - """Produce an instace of the correct species type. - - This function effectively produces the instance of the correct - Species Class based upon the arguments passed in. - - Cases: Here species refer to the Species in the Species list passed - into the construct. - - 1. No Species have parents. - Produces: an ComplexSpecies or an OrderedComplexSepcies - - 2. A single Species S has a parent which is an - OrderedPolymerSpecies with no parent. Produces: an - OrderedPolymerSpecies with a ComplexSpecies or - OrderedComplexSpecies containing S in S's location in the - OrderedPolymerSpecies. - - 3. [Error Case] Multiple Species S have parents which are - OrderedPolymerSpecies without parents. - - 4. [Error Case] Entire OrderedPolymerSpecies inside - PolymerConformations are being Complexed Together. - - 5. One or More Species S have parents which are - OrderedPolymerSpecies with parents and/or PolymerConformations. - - Produces: a (Ordered)ComplexSpecies containing all S inside a - PolymerConformation which merges all PolymerComformation Complexes. + """Create an instance of the appropriate species type. + + This method analyzes the input species and their parent + relationships to determine which type of complex to create. + + Parameters + ---------- + *args + Positional arguments, first should be the species list. + **kwargs + Keyword arguments including 'species' and 'ordered'. + + Returns + ------- + ComplexSpecies, OrderedComplexSpecies, OrderedPolymerSpecies, or + PolymerConformation + The appropriate species type based on input structure. + + Raises + ------ + TypeError + If species argument is not a list, or if trying to complex + entire OrderedPolymerSpecies that are already in + PolymerConformations, or if invalid parent types are found. + ValueError + If trying to form complexes between monomers from multiple + OrderedPolymerSpecies without PolymerConformations. + + Notes + ----- + Cases handled: + + 1. No Species have parents -> `ComplexSpecies` or + 1OrderedComplexSpecies` + 2. Single Species has parent `OrderedPolymerSpecies` (no parent) -> + `OrderedPolymerSpecies` with complex at binding site + 3. Multiple Species with OrderedPolymerSpecies1` parents (no + parents) -> Error (must use PolymerConformations) + 4. Entire OrderedPolymerSpecies in PolymerConformations -> Error + 5. One or more `Species` from polymer Conformations -> + `PolymerConformation` merging all complexes """ species = [] diff --git a/biocrnpyler/mechanisms/__init__.py b/biocrnpyler/mechanisms/__init__.py index bc712136..41ae4ae0 100644 --- a/biocrnpyler/mechanisms/__init__.py +++ b/biocrnpyler/mechanisms/__init__.py @@ -9,6 +9,7 @@ """ from .binding import * +from .conformation import * from .enzyme import * from .global_mechanisms import * from .integrase import * diff --git a/biocrnpyler/mechanisms/binding.py b/biocrnpyler/mechanisms/binding.py index c7489960..21175de2 100644 --- a/biocrnpyler/mechanisms/binding.py +++ b/biocrnpyler/mechanisms/binding.py @@ -9,9 +9,76 @@ class One_Step_Cooperative_Binding(Mechanism): - """A reaction where n binders (A) bind to 1 bindee (B) in one step. + """Cooperative binding mechanism for single-step multi-ligand binding. + + A 'binding' mechanism where multiple copies of a binder molecule (A) bind + cooperatively to a single bindee molecule (B) in one concerted step. This + models cooperative binding events where all ligands bind simultaneously + rather than sequentially. + + The binding reaction is given by + + n*A + B <--> A_n:B + + where n is the cooperativity (number of binders). + + Parameters + ---------- + name : str, default='one_step_cooperative_binding' + Name identifier for this mechanism instance. + mechanism_type : str, default='cooperative_binding' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('cooperative_binding'). + + See Also + -------- + Two_Step_Cooperative_Binding : Sequential cooperative binding mechanism. + Combinatorial_Cooperative_Binding : Multiple distinct binders binding. + One_Step_Binding : Simple binding without cooperativity. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is used to model cooperative binding where multiple + identical ligands bind simultaneously to a receptor. Common examples + include: + + - Oxygen binding to hemoglobin + - Transcription factor dimers binding to DNA + - Cooperative enzyme-substrate interactions + + The mechanism generates a single reversible mass-action reaction with + forward rate constant 'kb' and reverse rate constant 'ku'. The + 'cooperativity' parameter determines the stoichiometry of the binding + reaction. A cooperativity of 2 models dimeric binding, 3 for trimeric, + etc. + + Required parameters for this mechanism: + + - 'kb' : Forward binding rate constant + - 'ku' : Reverse unbinding rate constant + - 'cooperativity' : Number of binder molecules that bind simultaneously + + Examples + -------- + Create a mechanism for dimeric transcription factor binding: + + >>> promoter = bcp.RegulatedPromoter( + ... name='p_dimer', + ... regulators='TF_dimer', + ... ) + >>> mixture = bcp.Mixture( + ... components=[promoter], + ... mechanisms={'binding': bcp.One_Step_Cooperative_Binding()}, + ... parameters={'cooperativity': 2, 'kb': 0.1, 'ku': 0.01} + ... ) - n A + B <--> nA:B """ def __init__( @@ -29,8 +96,56 @@ def update_species( cooperativity=None, component=None, part_id=None, - **kwords, + **kwargs, ): + """Generate species for cooperative binding. + + Creates the species involved in cooperative binding: the binder, + bindee, and the resulting complex containing multiple binders bound + to the bindee. + + Parameters + ---------- + binder : Species + The ligand species that binds cooperatively. + bindee : Species + The target species being bound to. + complex_species : Species, optional + Pre-specified complex species. If None, automatically creates a + Complex containing n binders and 1 bindee, where n is the + cooperativity. + cooperativity : int or float, optional + Number of binder molecules that bind simultaneously. If None, + retrieved from component parameters using part_id. + component : Component, optional + Component containing parameter values. Required if cooperativity + is not provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + 'repr(binder)-repr(bindee)'. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [binder, bindee, complex] where complex is the + cooperative binding product. + + Raises + ------ + ValueError + If neither component nor cooperativity is provided. + TypeError + If complex_species is not a Species or None. + + Notes + ----- + The `cooperativity` parameter determines how many binder molecules + are incorporated into the complex. For example, cooperativity=2 + creates a complex with 2 binders and 1 bindee. + + """ if part_id is None: part_id = repr(binder) + '-' + repr(bindee) @@ -69,9 +184,66 @@ def update_reactions( ku=None, part_id=None, cooperativity=None, - **kwords, + **kwargs, ): - # Get Parameters + """Generate reactions for cooperative binding. + + Creates a single reversible mass-action reaction for the cooperative + binding of multiple binder molecules to a bindee. + + Parameters + ---------- + binder : Species + The ligand species that binds cooperatively. + bindee : Species + The target species being bound to. + complex_species : Species, optional + Pre-specified complex species. If None, automatically creates a + Complex containing n binders and 1 bindee. + component : Component, optional + Component containing parameter values. Required if kb, ku, or + cooperativity are not provided directly. + kb : Parameter or float, optional + Forward binding rate constant. If None, retrieved from component + parameters. + ku : Parameter or float, optional + Reverse unbinding rate constant. If None, retrieved from + component parameters. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + 'repr(binder)-repr(bindee)'. + cooperativity : int or float, optional + Number of binder molecules that bind simultaneously. If None, + retrieved from component parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single reversible mass-action reaction for + cooperative binding. + + Raises + ------ + ValueError + If component is None and any of kb, ku, or cooperativity is not + provided. + TypeError + If complex_species is not a Species or None. + + Notes + ----- + The reaction stoichiometry is determined by the `cooperativity` + parameter: + + - cooperativity * binder + bindee <--> complex + + The forward rate is `kb` and reverse rate is `ku`. The reaction + follows mass-action kinetics with the appropriate stoichiometric + coefficients. + + """ if part_id is None: part_id = repr(binder) + '-' + repr(bindee) if kb is None and component is not None: @@ -126,10 +298,83 @@ def update_reactions( class Two_Step_Cooperative_Binding(Mechanism): - """A reaction where n binders (s1) bind to 1 bindee (s2) in two steps. + """Sequential cooperative binding mechanism with oligomerization. + + A 'binding' mechanism where multiple binder molecules first oligomerize, + then the oligomer binds to the bindee in a two-step process. This models + cooperative binding where ligands must first form a multimeric complex + before binding to their target. + + The binding process follows two sequential reactions: + + 1. n*A <--> A_n (oligomerization) + 2. A_n + B <--> A_n:B (binding) + + where n is the cooperativity. + + Parameters + ---------- + name : str, default='two_step_cooperative_binding' + Name identifier for this mechanism instance. + mechanism_type : str, default='cooperative_binding' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('cooperative_binding'). + + See Also + -------- + One_Step_Cooperative_Binding : Single-step cooperative binding. + Combinatorial_Cooperative_Binding : Multiple distinct binders. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models cooperative binding as a two-step process: + + 1. Oligomerization: Binder molecules first associate to form an + oligomer (dimer, trimer, etc.) + 2. Binding: The oligomer then binds to the target + + This is useful for modeling: + + - Protein dimerization followed by DNA binding + - Receptor oligomerization before ligand binding + - Sequential assembly and binding processes + + Required parameters for this mechanism: + + - 'kb1' : Forward rate constant for oligomerization + - 'ku1' : Reverse rate constant for oligomerization + - 'kb2' : Forward rate constant for oligomer-bindee binding + - 'ku2' : Reverse rate constant for oligomer-bindee binding + - 'cooperativity' : Number of binder molecules in the oligomer + + Examples + -------- + Model transcription factor dimerization followed by DNA binding: + + >>> mech = bcp.Two_Step_Cooperative_Binding() + >>> # TF dimerizes (2*TF <-> TF2), then binds DNA (TF2 + DNA <-> TF2:DNA) + >>> params = { + ... 'cooperativity': 2, + ... 'kb1': 0.1, 'ku1': 0.01, # Dimerization rates + ... 'kb2': 1.0, 'ku2': 0.001 # DNA binding rates + ... } + + Model trimeric receptor assembly and activation: + + >>> mech = bcp.Two_Step_Cooperative_Binding() + >>> params = { + ... 'cooperativity': 3, # Trimeric receptor + ... 'kb1': 0.05, 'ku1': 0.1, # Trimerization + ... 'kb2': 10.0, 'ku2': 0.01 # Ligand binding + ... } - n A <--> nx_A - nx_A <--> nx_A:B """ def __init__( @@ -148,8 +393,57 @@ def update_species( n_mer_species=None, cooperativity=None, part_id=None, - **keywords, + **kwargs, ): + """Generate species for two-step cooperative binding. + + Creates the species involved in sequential cooperative binding: + binder, bindee, oligomer (n-mer), and final complex. + + Parameters + ---------- + binder : Species + The ligand species that oligomerizes then binds. + bindee : Species + The target species that the oligomer binds to. + component : Component, optional + Component containing parameter values. Required if cooperativity + is not provided directly. + complex_species : Species, optional + Pre-specified final complex species. If None, automatically + creates a Complex containing the n-mer and bindee. + n_mer_species : Species, optional + Pre-specified oligomer species. If None, automatically creates a + Complex containing n binders. + cooperativity : int or float, optional + Number of binders in the oligomer. If None, retrieved from + component parameters. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + 'repr(binder)-repr(bindee)'. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [binder, bindee, complex, n_mer] where n_mer is + the oligomer and complex is the final bound product. + + Raises + ------ + ValueError + If neither component nor cooperativity is provided. + TypeError + If n_mer_species or complex_species is not a Species or None. + + Notes + ----- + The n_mer represents the oligomerized form of the binder (e.g., a + dimer for cooperativity=2). The complex represents the n_mer bound + to the bindee. + + """ if part_id is None: part_id = repr(binder) + '-' + repr(bindee) @@ -201,20 +495,66 @@ def update_reactions( cooperativity=None, complex_species=None, n_mer_species=None, - **keywords, + **kwargs, ): - """Update reactions. - - Returns reactions: - cooperativity binder <--> n_mer, kf = kb1, kr = ku1 - n_mer + bindee <--> complex, kf = kb2, kr = ku2 - :param s1: - :param s2: - :param kb: - :param ku: - :param cooperativity: - :param keywords: - :return: + """Generate reactions for two-step cooperative binding. + + Creates two sequential reactions: oligomerization of binders followed + by oligomer binding to the bindee. + + Parameters + ---------- + binder : Species + The ligand species that oligomerizes then binds. + bindee : Species + The target species that the oligomer binds to. + kb : list of float or Parameter, optional + Forward rate constants [kb1, kb2] for oligomerization and binding. + If None, retrieved from component parameters. + ku : list of float or Parameter, optional + Reverse rate constants [ku1, ku2] for oligomerization and binding. + If None, retrieved from component parameters. + component : Component, optional + Component containing parameter values. Required if kb, ku, or + cooperativity are not provided. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + 'repr(binder)-repr(bindee)'. + cooperativity : int or float, optional + Number of binders in the oligomer. If None, retrieved from + component parameters. + complex_species : Species, optional + Pre-specified final complex species. + n_mer_species : Species, optional + Pre-specified oligomer species. + **kwargs + Additional keyword arguments passed to update_species. + + Returns + ------- + list of Reaction + List containing two reactions: + + 1. Oligomerization: cooperativity*binder <--> n_mer + 2. Binding: n_mer + bindee <--> complex + + Raises + ------ + ValueError + If component is None and `kb`, `ku`, or `cooperativity` is not + provided, or if `kb` and `ku` do not contain exactly 2 values + each. + + Notes + ----- + The two-step process uses separate rate constants: + + - 'kb1', 'ku1': Control oligomerization kinetics + - 'kb2', 'ku2': Control oligomer-bindee binding kinetics + + This separation allows modeling of processes where oligomerization + and binding have different kinetic properties. + """ if part_id is None: repr(binder) + '-' + repr(bindee) @@ -262,7 +602,7 @@ def update_reactions( n_mer_species=n_mer_species, cooperativity=cooperativity, part_id=part_id, - **keywords, + **kwargs, ) inputs_for_rxn1 = [ @@ -287,28 +627,122 @@ def update_reactions( class Combinatorial_Cooperative_Binding(Mechanism): - """Reaction where some number binders bind combinatorially to bindee.""" + """Combinatorial binding mechanism for multiple distinct ligands. + + A 'binding' mechanism where different types of binder molecules can bind + to a bindee in various combinations, each with its own cooperativity. This + models complex regulatory scenarios where multiple transcription factors + or ligands can bind to the same target in different combinations, each + producing a distinct complex. + + The mechanism generates all possible binding combinations and the + reactions between them, considering individual binding affinities and + cooperativities for each binder type. + + Parameters + ---------- + name : str, default='Combinatorial_Cooperative_binding' + Name identifier for this mechanism instance. + mechanism_type : str, default='cooperative_binding' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('cooperative_binding'). + + See Also + -------- + One_Step_Cooperative_Binding : Single binder type cooperative binding. + CombinatorialPromoter : Component that uses this mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is designed for modeling complex regulatory logic where: + + - Multiple different regulators can bind to the same target + - Each regulator can have its own cooperativity (e.g., some bind as + dimers, others as monomers) + - All possible combinations of bound states are generated + - Each transition between states has specific rate constants + + The mechanism generates a complete reaction network connecting all + possible bound states. For n different binders, this creates 2^n - 1 + different complexes (excluding the unbound state). + + Required parameters for this mechanism (per binder): + + - 'kb': Forward binding rate + - 'ku': Reverse unbinding rate + - 'cooperativity': Number of molecules binding together + + This is commonly used for: + + - Complex promoter regulation with multiple transcription factors + - Multi-ligand receptor systems + - Combinatorial protein complex assembly + + Examples + -------- + Model a promoter with two different transcription factors: + + >>> A, B = bcp.Species('A'), bcp.Species('B') + >>> AND_promoter = bcp.CombinatorialPromoter( + ... 'AND_promoter', [A, B], tx_capable_list=[[A, B]], leak=False) + ... AND_assembly = bcp.DNAassembly( + ... 'AND', promoter=AND_promoter, rbs='medium', protein='GFP') + ... mixture = bcp.ExpressionExtract( + ... name='AND_mixture', components=[AND_assembly], + ... parameter_file=[ + ... 'mechanisms/binding_parameters.tsv', + ... 'mixtures/extract_parameters.tsv', + ... ] + ... ) + ... crn = mixture.compile_crn() + + """ def __init__( self, name='Combinatorial_Cooperative_binding', mechanism_type='cooperative_binding', ): - """Initializes a Combinatorial_Cooperative_Binding instance. - - :param name: name of the Mechanism, default: \ - Combinatorial_Cooperative_binding - :param mechanism_type: type of the Mechanism, default: \ - cooperative_binding - """ Mechanism.__init__(self, name, mechanism_type) def make_cooperative_complex(self, combo, bindee, cooperativity): - """Make complex containing multipple binders. + """Create a complex with multiple cooperative binders. - Given a list of binders and their cooperativities, make a complex that - contains the binders present N number of times where N is each one's - cooperativity. + Constructs a complex species containing the specified combination of + binders (each repeated according to its cooperativity) bound to the + bindee. + + Parameters + ---------- + combo : tuple or list of Species + Combination of binder species to include in the complex. + bindee : Species + The target species being bound to. + cooperativity : dict + Dictionary mapping binder names (str) to their cooperativity + values (int). Determines how many copies of each binder are + included. + + Returns + ------- + Species or Complex + If only bindee is present (empty combo), returns bindee alone. + Otherwise returns a Complex containing all binders (repeated per + cooperativity) and the bindee. + + Notes + ----- + For each binder in combo, the method adds cooperativity[binder.name] + copies to the complex. For example, if binder A has cooperativity 2 + and binder B has cooperativity 1, the complex for combo=[A, B] would + contain [A, A, B, bindee]. """ complexed_species_list = [] @@ -333,12 +767,59 @@ def update_species( cooperativity=None, component=None, part_id=None, - **kwords, + **kwargs, ): + """Generate all species for combinatorial binding. + + Creates all possible complexes from combinations of binders bound to + the bindee, considering each binder's cooperativity. + + Parameters + ---------- + binders : list of Species + List of different binder species that can bind in combinations. + bindee : Species + The target species being bound to. + cooperativity : dict, optional + Dictionary mapping binder names to cooperativity values. If None + for any binder, retrieved from component parameters. + component : Component, optional + Component containing parameter values. Required if cooperativity + values are not provided. + part_id : str, optional + Base identifier for parameter lookup. Individual binder parameters + are looked up as 'part_id_bindername'. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List of all possible complexes from binding combinations. For n + binders, returns 2^n - 1 complexes (all combinations except + unbound bindee). + + Raises + ------ + ValueError + If component is None and cooperativity is not provided for all + binders. + + Notes + ----- + This method generates all possible combinations of binders: + - Single binders: A:bindee, B:bindee, etc. + - Pairs: A:B:bindee, A:C:bindee, etc. + - Higher combinations up to all binders bound simultaneously + + Each complex respects the individual cooperativity of its binders. + + """ cooperativity_dict = {} out_species = [] + prefix = "" if part_id is None else part_id + '_' for binder in binders: - binder_partid = part_id + '_' + binder.name + binder_partid = prefix + binder.name if ( (cooperativity is None) or ( @@ -388,11 +869,76 @@ def update_reactions( kus=None, part_id=None, cooperativity=None, - **kwords, + **kwargs, ): + """Generate reactions for all combinatorial binding transitions. + + Creates reactions connecting all possible binding states, where each + reaction represents one binder type associating or dissociating while + other binders remain bound. + + Parameters + ---------- + binders : list of Species + List of different binder species that can bind in combinations. + bindee : Species + The target species being bound to. + component : Component, optional + Component containing parameter values. Required if rate constants + or cooperativities are not provided. + kbs : dict, optional + Dictionary mapping binder names to forward rate constants (kb). + If None for any binder, retrieved from component parameters. + kus : dict, optional + Dictionary mapping binder names to reverse rate constants (ku). + If None for any binder, retrieved from component parameters. + part_id : str, optional + Base identifier for parameter lookup. Individual binder parameters + are looked up as 'part_id_bindername'. + cooperativity : dict, optional + Dictionary mapping binder names to cooperativity values. If None + for any binder, retrieved from component parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of all reactions connecting binding states. Each reaction + represents adding or removing one binder type to/from an existing + complex. + + Raises + ------ + ValueError + If component is None and any required parameters (kb, ku, + cooperativity) are not provided. + + Notes + ----- + The reaction network connects all possible binding states such that: + + - Each reaction adds or removes exactly one binder type + - Rate constants are specific to each binder + - Cooperativity determines the stoichiometry of each binder + + For n binders, this generates approximately n * 2^(n-1) reactions, + connecting the 2^n possible states (including unbound). + + Each binder requires three parameters: + + - 'kb': Forward binding rate constant + - 'ku': Reverse unbinding rate constant + - 'cooperativity': Stoichiometry of that binder + + The algorithm avoids generating duplicate reactions by tracking which + transitions have been created. + + """ binder_params = {} + prefix = "" if part_id is None else part_id + '_' for binder in binders: - binder_partid = part_id + '_' + binder.name + binder_partid = prefix + binder.name if (isinstance(kbs, dict) and binder not in kbs) or ( not isinstance(kbs, dict) and component is not None ): @@ -504,9 +1050,86 @@ def update_reactions( class One_Step_Binding(Mechanism): - """A mechanism to model the binding of a list of species. + """Simple binding mechanism for multiple species without cooperativity. + + A 'binding' mechanism to model the simultaneous binding of multiple + species into a single complex in one concerted step. Unlike cooperative + binding mechanisms, this treats all species equally without cooperativity + factors - each species contributes exactly one molecule to the complex. + + The binding reaction follows: + S1 + S2 + ... + Sn <--> S1:S2:...:Sn + + Parameters + ---------- + name : str, default='one_step_binding' + Name identifier for this mechanism instance. + mechanism_type : str, default='binding' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('binding'). + + See Also + -------- + One_Step_Cooperative_Binding : Binding with cooperativity. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is the simplest binding model where multiple distinct + species come together to form a complex. Each species contributes + exactly one molecule (stoichiometry of 1) to the complex. + + Common applications include: + + - Protein complex formation from distinct subunits + - Multi-component enzyme assembly + - Simple receptor-ligand binding + - Formation of heterodimers or heterotrimers + + The mechanism generates a single reversible mass-action reaction with + rate constants 'kb' (forward) and 'ku' (reverse). + + Key differences from cooperative binding: + + - No 'cooperativity' parameter - all stoichiometries are 1 + - Can handle arbitrary lists of different species + - Simpler parameter structure (single 'kb', 'ku' for the entire reaction) + + Required parameters for this mechanism: + + - 'kb' : Forward binding rate constant + - 'ku' : Reverse unbinding rate constant + + Examples + -------- + Model receptor-ligand binding: + + >>> mech = bcp.One_Step_Binding() + >>> ligand, receptor = bcp.Species('L'), bcp.Species('R') + >>> mech.update_species( + ... binder=ligand, + ... bindee=receptor, + ... kb=1.0, ku=0.001 + ... ) + [L, R, complex_L_R_] + + Model formation of a three-protein complex: + + >>> proteins = [ + ... bcp.Species(s, material_type='protein') for s in ['A', 'B', 'C']] + >>> rxns = mech.update_reactions( + ... binder=proteins[0], + ... bindee=proteins[1:], + ... kb=0.1, ku=0.01 + ... ) + >>> # Generates: A + B + C <--> A:B:C - S1 + S2 ... SN <--> S1:S2:...:SN """ def __init__(self, name='one_step_binding', mechanism_type='binding'): @@ -519,8 +1142,49 @@ def update_species( component=None, complex_species=None, part_id=None, - **keywords, + **kwargs, ): + """Generate species for simple multi-species binding. + + Creates the list of species involved in the binding reaction: all + input species plus the resulting complex. + + Parameters + ---------- + binder : Species or list of Species + The first species or list of species to bind. Automatically + converted to list if single species provided. + bindee : Species or list of Species + The second species or list of species to bind. Automatically + converted to list if single species provided. + component : Component, optional + Component containing this mechanism (unused but kept for API + consistency). + complex_species : Species, optional + Pre-specified complex species. If None, automatically creates a + Complex containing all binders and bindees. + part_id : str, optional + Identifier for parameter lookup. If None, automatically generated + from species names as `name1_name2_..._nameN`. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing all input species plus the complex. Format: + [binder1, ..., bindee1, ..., complex]. + + Notes + ----- + The binder/bindee distinction is primarily for API consistency with + other binding mechanisms. Functionally, all species are treated + equally in the binding reaction. + + The complex is created as a Complex object containing all input + species in the order: binders + bindees. + + """ if not isinstance(binder, list): binder = [binder] if not isinstance(bindee, list): @@ -546,8 +1210,59 @@ def update_reactions( part_id=None, kb=None, ku=None, - **keywords, + **kwargs, ): + """Generate reaction for simple multi-species binding. + + Creates a single reversible mass-action reaction for the binding of + all species into a complex. + + Parameters + ---------- + binder : Species or list of Species + The first species or list of species to bind. Automatically + converted to list if single species provided. + bindee : Species or list of Species + The second species or list of species to bind. Automatically + converted to list if single species provided. + component : Component, optional + Component containing parameter values. Required if kb or ku are + not provided directly. + complex_species : Species, optional + Pre-specified complex species. If None, automatically creates a + Complex containing all binders and bindees. + part_id : str, optional + Identifier for parameter lookup. If None, automatically generated + from species names as `name1_name2_..._nameN`. + kb : Parameter or float, optional + Forward binding rate constant. If None, retrieved from component + parameters. + ku : Parameter or float, optional + Reverse unbinding rate constant. If None, retrieved from + component parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single reversible mass-action reaction for the + binding of all species. + + Raises + ------ + ValueError + If component is None and kb or ku is not provided. + + Notes + ----- + The reaction has equal stoichiometry (1) for all species: + species1 + species2 + ... + speciesN <--> complex + + This is simpler than cooperative binding mechanisms which can have + varying stoichiometries for different species. + + """ if not isinstance(binder, list): binder = [binder] if not isinstance(bindee, list): diff --git a/biocrnpyler/mechanisms/binding_parameters.tsv b/biocrnpyler/mechanisms/binding_parameters.tsv new file mode 100644 index 00000000..726967ba --- /dev/null +++ b/biocrnpyler/mechanisms/binding_parameters.tsv @@ -0,0 +1,7 @@ +mechanism_id part_id param_name param_val units comments + ktx 0.05 transcripts/sec/polymerase Assuming 50nt/s and transcript length of 1000 + ktl 0.05 proteins/sec/nM Assuming 15aa/s and protein length of 300 + cooperativity 2 + kb 100 assuming 10ms to diffuse across 1um (characteristic cell size) + ku 10 90% binding + kdil 0.001 assuming half life of ~20 minutes for everything (e coli doubling time) diff --git a/biocrnpyler/mechanisms/conformation.py b/biocrnpyler/mechanisms/conformation.py index 77e5c73e..da7ab284 100644 --- a/biocrnpyler/mechanisms/conformation.py +++ b/biocrnpyler/mechanisms/conformation.py @@ -6,9 +6,57 @@ class One_Step_Reversible_Conformation_Change(Mechanism): - """A reaction where n binders (A) bind to 1 bindee (B) in one step. + """Reversible conformational change mechanism. + + A mechanism that models the reversible conformational change of a species + from one state to another. This can represent protein folding/unfolding, + DNA structural changes, allosteric transitions, or any other molecular + conformational switch. Additional species (cofactors, ions, etc.) can be + required for the conformational change. + + The reaction follows: + + s0 [+ additional species] <--> sf [+ additional species] + + where s0 is the initial conformation and sf is the final conformation. + + Parameters + ---------- + name : str, default='one_step_conformation_change' + Name identifier for this mechanism instance. + mechanism_type : str, default='conformation_change' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('conformation_change'). + + See Also + -------- + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is used to model conformational changes including: + + - Protein folding: Native <--> denatured states + - Allosteric transitions: Inactive <--> active enzyme forms + - DNA structural changes: B-DNA <--> Z-DNA transitions + - Receptor activation: Closed <--> open channel states + - Molecular switches: Any two-state molecular system + + The mechanism requires two rate constants: + + - 'kf': Forward rate constant (s0 -> sf) + - 'kr': Reverse rate constant (sf -> s0) + + Additional species can participate in the conformational change without + being modified (e.g., ATP required for a conformational switch but not + consumed). - n A + B <--> nA:B """ def __init__( @@ -16,6 +64,11 @@ def __init__( name='one_step_conformation_change', mechanism_type='conformation_change', ): + """Initialize a One_Step_Reversible_Conformation_Change mechanism. + + See class docstring for parameter descriptions. + + """ Mechanism.__init__(self, name, mechanism_type) def update_species( @@ -25,8 +78,46 @@ def update_species( additional_species=None, component=None, part_id=None, - **kwords, + **kwargs, ): + """Generate species for conformational change. + + Creates the list of species involved in the conformational change, + including initial and final conformations plus any additional species + required for the transition. + + Parameters + ---------- + s0 : Species + The initial conformation state of the molecule. + sf : Species + The final conformation state of the molecule. + additional_species : list of Species, optional + Additional species required for the conformational change (e.g., + cofactors, ions) but not consumed. Default is empty list. + component : Component, optional + Component containing this mechanism (for context, not used here). + part_id : str, optional + Identifier for parameter lookup (not used in species generation). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [s0, sf] plus any additional species. + + Notes + ----- + The additional species participate in the conformational change but + are not modified. They appear on both sides of the reaction. This is + useful for modeling: + + - Cofactor-dependent conformational changes + - Ion-induced structural transitions + - Ligand-stabilized conformations + + """ if additional_species is None: additional_species = [] @@ -39,10 +130,63 @@ def update_reactions( additional_species=None, component=None, part_id=None, - **kwords, + **kwargs, ): + """Generate reactions for conformational change. + + Creates a reversible mass-action reaction for the conformational + transition between two states of a molecule. + + Parameters + ---------- + s0 : Species + The initial conformation state of the molecule. + sf : Species + The final conformation state of the molecule. + additional_species : list of Species, optional + Additional species required for the conformational change (e.g., + cofactors, ions) but not consumed. These appear on both sides of + the reaction. Default is empty list. + component : Component + Component containing parameter values. Required for retrieving + 'kf' and 'kr' rate constants. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + 'repr(s0)-repr(sf)'. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single reversible mass-action reaction for the + conformational change. + + Raises + ------ + ValueError + If component is None (implicitly, when parameters cannot be + retrieved). + + Notes + ----- + The reaction equation depends on additional_species: + + - Without additional species: s0 <--> sf + - With additional species: s0 + additionals <--> sf + additionals + + The mechanism requires two parameters: + + - 'kf': Forward rate constant (s0 -> sf transition) + - 'kr': Reverse rate constant (sf -> s0 transition) + + Additional species are not consumed or produced; they facilitate the + conformational change. This models situations where cofactors or ions + must be present for the transition but are not modified. + + """ if part_id is None: - repr(s0) + '-' + repr(sf) + part_id = repr(s0) + '-' + repr(sf) if additional_species is None: additional_species = [] diff --git a/biocrnpyler/mechanisms/enzyme.py b/biocrnpyler/mechanisms/enzyme.py index 994084d1..4099dbf3 100644 --- a/biocrnpyler/mechanisms/enzyme.py +++ b/biocrnpyler/mechanisms/enzyme.py @@ -7,19 +7,100 @@ class BasicCatalysis(Mechanism): - """Mechanism for the schema S + C --> P + C.""" + """Basic catalytic mechanism for irreversible substrate conversion. + + A 'catalysis' mechanism where a catalyst (enzyme) converts a substrate + into a product in a single irreversible step. The catalyst is not + consumed in the reaction and can continue to catalyze additional + conversions. + + The catalytic reaction is given by + + S + C --> P + C + + where S is the substrate, C is the catalyst (enzyme), and P is the + product. + + Parameters + ---------- + name : str, default='basic_catalysis' + Name identifier for this mechanism instance. + mechanism_type : str, default='catalysis' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('catalysis'). + + See Also + -------- + BasicProduction : Catalytic production without substrate consumption. + MichaelisMenten : Two-step enzyme kinetics with complex formation. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction + with rate constant 'kcat'. Unlike Michaelis-Menten kinetics, there is + no explicit enzyme-substrate complex formation; the reaction proceeds + in a single catalytic step. + + Common applications include: + + - Simplified enzyme kinetics models + - Catalytic degradation reactions + - Rate-limiting steps in metabolic pathways + + Required parameters for this mechanism: + + - 'kcat' : Catalytic rate constant for substrate conversion + + Examples + -------- + Model enzymatic degradation of a substrate: + + >>> enzyme = bcp.Enzyme('E', substrates=['S'], products=['P']) + >>> mixture = bcp.Mixture( + ... components=[enzyme], + ... mechanisms={'catalysis': bcp.BasicCatalysis()}, + ... parameters={'kcat': 1.0} + ... ) + >>> mixture.compile_crn() + + """ def __init__( self, name: str = 'basic_catalysis', mechanism_type: str = 'catalysis' ): - """Initializes a BasicCatalysis instance. - - :param name: name of the Mechanism, default: 'basic_catalysis' - :param mechanism_type: type of the Mechanism, default: 'catalysis' - """ Mechanism.__init__(self, name, mechanism_type) def update_species(self, enzyme, substrate, product=None): + """Generate species for basic catalysis. + + Creates the list of species involved in the catalytic reaction: + enzyme, substrate, and optionally the product. + + Parameters + ---------- + enzyme : Species + The catalyst species that facilitates the reaction. + substrate : Species + The substrate species to be converted. + product : Species, optional + The product species. If None, only enzyme and substrate are + returned (useful for degradation reactions where no explicit + product is tracked). + + Returns + ------- + list of Species + List containing [enzyme, substrate] if product is None, or + [enzyme, substrate, product] otherwise. + + """ if product is None: return [enzyme, substrate] else: @@ -34,6 +115,47 @@ def update_reactions( part_id=None, kcat=None, ): + """Generate reactions for basic catalysis. + + Creates a single irreversible mass-action reaction for catalytic + conversion of substrate to product. + + Parameters + ---------- + enzyme : Species + The catalyst species that facilitates the reaction. + substrate : Species + The substrate species to be converted. + product : Species + The product species. Can be None for degradation reactions. + component : Component, optional + Component containing parameter values. Required if kcat is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + kcat : Parameter or float, optional + Catalytic rate constant. If None, retrieved from component + parameters. + + Returns + ------- + list of Reaction + List containing a single irreversible mass-action reaction: + enzyme + substrate --> enzyme + product. + + Raises + ------ + ValueError + If component is None and kcat is not provided. + + Notes + ----- + The reaction follows mass-action kinetics with rate constant 'kcat'. + The enzyme appears on both sides of the reaction as it acts as a + catalyst and is not consumed. + + """ if part_id is None and component is not None: part_id = component.name @@ -57,18 +179,113 @@ def update_reactions( class BasicProduction(Mechanism): - """Mechanism for the schema C --> P + C.""" + """Basic catalytic production mechanism with optional substrate. - def __init__(self, name='basic_production', mechanism_type='catalysis'): - """Initializes a BasicProduction instance. + A 'catalysis' mechanism where a catalyst (enzyme) produces a product. + Optionally, a substrate can be consumed during production, allowing for + both pure production (C --> P + C) and production with substrate + consumption (S + C --> P + C). - :param name: name of the Mechanism, default: basic_production - :param mechanism_type: type of the Mechanism, default: catalysis - :param keywords: - """ + The production reaction can be either: + + C --> P + C (pure production, no substrate) + + or + + S + C --> P + C (production with substrate consumption) + + where S is the substrate, C is the catalyst (enzyme), and P is the + product. + + Parameters + ---------- + name : str, default='basic_production' + Name identifier for this mechanism instance. + mechanism_type : str, default='catalysis' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('catalysis'). + + See Also + -------- + BasicCatalysis : Catalytic conversion requiring a substrate. + MichaelisMentenCopy : Two-step kinetics preserving the substrate. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction + with rate constant 'kcat'. The catalyst is not consumed and appears on + both sides of the reaction. + + Common applications include: + + - Constitutive gene expression (transcription/translation) + - Enzymatic synthesis reactions + - Autocatalytic production processes + + Required parameters for this mechanism: + + - 'kcat' : Catalytic rate constant for product formation + + The flexibility to include or exclude substrates makes this mechanism + useful for modeling both simple production (e.g., constitutive protein + expression) and production coupled with substrate consumption (e.g., + enzymatic synthesis from precursors). + + Examples + -------- + Model constitutive protein production from a gene: + + >>> gene = bcp.DNA('gfp') + >>> protein = bcp.Protein('GFP') + >>> expression = bcp.Enzyme(gene, substrates=[], products=[protein]) + >>> mixture = bcp.Mixture( + ... components=[expression], + ... mechanisms={'catalysis': bcp.BasicProduction()}, + ... parameters={'kcat': 0.01} + ... ) + >>> mixture.compile_crn() + Species = dna_gfp, protein_GFP + Reactions = [ + dna[gene] --> dna[gene]+protein[protein] + ] + + """ + + def __init__(self, name='basic_production', mechanism_type='catalysis'): Mechanism.__init__(self, name, mechanism_type) def update_species(self, enzyme, substrate=None, product=None): + """Generate species for basic production. + + Creates the list of species involved in the production reaction: + enzyme, and optionally substrate and product. + + Parameters + ---------- + enzyme : Species + The catalyst species that facilitates production. + substrate : Species, optional + The substrate species to be consumed. If None, production + occurs without substrate consumption. + product : Species, optional + The product species. If None, only enzyme (and substrate if + provided) are returned. + + Returns + ------- + list of Species + List containing enzyme and any non-None substrate and product + species. Order is [enzyme, product, substrate] if all are + provided. + + """ species = [enzyme] if product is not None: species += [product] @@ -86,6 +303,51 @@ def update_reactions( part_id=None, kcat=None, ): + """Generate reactions for basic production. + + Creates a single irreversible mass-action reaction for catalytic + production, with or without substrate consumption. + + Parameters + ---------- + enzyme : Species + The catalyst species that facilitates production. + substrate : Species + The substrate species. Can be None for pure production without + substrate consumption. + product : Species + The product species. Can be None if no explicit product is + tracked. + component : Component, optional + Component containing parameter values. Required if kcat is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + kcat : Parameter or float, optional + Catalytic rate constant. If None, retrieved from component + parameters. + + Returns + ------- + list of Reaction + List containing a single irreversible mass-action reaction. + If substrate is None: enzyme --> enzyme + product. + If substrate is provided: enzyme + substrate --> enzyme + + product. + + Raises + ------ + ValueError + If component is None and kcat is not provided. + + Notes + ----- + The enzyme appears on both sides of the reaction as it acts as a + catalyst and is not consumed. The substrate, if provided, is + consumed in the reaction. + + """ if part_id is None and component is not None: part_id = component.name @@ -111,22 +373,131 @@ def update_reactions( class MichaelisMenten(Mechanism): - """Mechanism to automatically generate Michaelis-Menten Type Reactions. + """Standard Michaelis-Menten enzyme kinetics mechanism. + + A 'catalysis' mechanism implementing classical Michaelis-Menten enzyme + kinetics with explicit enzyme-substrate complex formation. The substrate + binds reversibly to the enzyme to form a complex, which then + irreversibly converts to product and releases the enzyme. + + The reaction scheme is + + S + E <--> S:E --> E + P + + where S is the substrate, E is the enzyme, S:E is the enzyme-substrate + complex, and P is the product. + + Parameters + ---------- + name : str, default='michaelis_menten' + Name identifier for this mechanism instance. + mechanism_type : str, default='catalysis' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('catalysis'). + + See Also + -------- + BasicCatalysis : Single-step catalysis without complex formation. + MichaelisMentenCopy : Michaelis-Menten preserving substrate. + MichaelisMentenReversible : Michaelis-Menten with product binding. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates two mass-action reactions: + + 1. Reversible binding: S + E <--> S:E (rates 'kb' and 'ku') + 2. Irreversible catalysis: S:E --> E + P (rate 'kcat') + + Common applications include: + + - Enzyme-catalyzed reactions in metabolic pathways + - Protein degradation by proteases + - Drug metabolism by cytochrome P450 enzymes + - Any enzymatic process following Michaelis-Menten kinetics + + Required parameters for this mechanism: + + - 'kb' : Binding rate constant for enzyme-substrate association + - 'ku' : Unbinding rate constant for enzyme-substrate dissociation + - 'kcat' : Catalytic rate constant for product formation + + The mechanism can also model degradation reactions by setting product + to None, resulting in: S + E <--> S:E --> E. + + Examples + -------- + Model enzyme-catalyzed substrate conversion: + + >>> substrate = bcp.Species('S') + >>> product = bcp.Species('P') + >>> enzyme = bcp.Enzyme('E', substrates=[substrate], products=[product]) + >>> mixture = bcp.Mixture( + ... components=[enzyme], + ... mechanisms={'catalysis': bcp.MichaelisMenten()}, + ... parameters={'kb': 1.0, 'ku': 0.1, 'kcat': 0.5} + ... ) + >>> mixture.compile_crn() + Species = protein_E, S, P, complex_S_protein_E_ + Reactions = [ + S+protein[E] <--> complex[S:protein[E]] + complex[S:protein[E]] --> P+protein[E] + ] + + Model enzymatic degradation: + + >>> degradase = bcp.Protein('degradase') + >>> target = bcp.Protein('target') + >>> degrader = bcp.Enzyme(degradase, substrates=[target], products=[]) + >>> mixture = bcp.Mixture( + ... components=[degrader], + ... mechanisms={'catalysis': bcp.MichaelisMenten()}, + ... parameters={'kb': 1.0, 'ku': 0.1, 'kcat': 0.2} + ... ) - In the Copy RXN version, the substrates is not Consumed: - sub + enz <--> sub:enz --> enz + prod """ def __init__(self, name='michaelis_menten', mechanism_type='catalysis'): - """Initializes a MichaelisMenten instance. - - :param name: name of the Mechanism, default: 'michaelis_menten' - :param mechanism_type: type of the Mechanism, default: 'catalysis' - :param keywords: - """ Mechanism.__init__(self, name, mechanism_type) def update_species(self, enzyme, substrate, product=None, complex=None): + """Generate species for Michaelis-Menten kinetics. + + Creates the species involved in Michaelis-Menten enzyme kinetics: + enzyme, substrate, enzyme-substrate complex, and optionally the + product. + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the reaction. + substrate : Species + The substrate species to be converted. + product : Species, optional + The product species. If None, only enzyme, substrate, and + complex are returned (useful for degradation reactions). + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + + Returns + ------- + list of Species + List containing [enzyme, substrate, complex] if product is + None, or [enzyme, substrate, product, complex] otherwise. + + Notes + ----- + The complex is automatically generated as a Complex object + containing the substrate and enzyme if not explicitly provided. + + """ if complex is None: complexS = Complex([substrate, enzyme]) else: @@ -148,6 +519,63 @@ def update_reactions( ku=None, kcat=None, ): + """Generate reactions for Michaelis-Menten kinetics. + + Creates two mass-action reactions implementing Michaelis-Menten + enzyme kinetics: reversible enzyme-substrate binding and + irreversible catalytic conversion. + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the reaction. + substrate : Species + The substrate species to be converted. + product : Species + The product species. Can be None for degradation reactions. + component : Component, optional + Component containing parameter values. Required if kb, ku, or + kcat are not provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + kb : Parameter or float, optional + Forward binding rate constant. If None, retrieved from + component parameters. + ku : Parameter or float, optional + Reverse unbinding rate constant. If None, retrieved from + component parameters. + kcat : Parameter or float, optional + Catalytic rate constant. If None, retrieved from component + parameters. + + Returns + ------- + list of Reaction + List containing two reactions: + [binding_reaction, catalysis_reaction]. + + Raises + ------ + ValueError + If component is None and any of kb, ku, or kcat is not + provided. + + Notes + ----- + The mechanism generates the following reactions: + + 1. S + E <--> S:E (binding, rates 'kb' and 'ku') + 2. S:E --> E + P (catalysis, rate 'kcat') + + For degradation (product is None): + + 2. S:E --> E (degradation, rate 'kcat') + + """ # Get parameters if part_id is None and component is not None: part_id = component.name @@ -195,10 +623,93 @@ def update_reactions( class MichaelisMentenReversible(Mechanism): - """Michaelis-Menten reactions with product that can bind to enzymes. + """Reversible Michaelis-Menten kinetics with product binding. + + A 'catalysis' mechanism implementing Michaelis-Menten enzyme kinetics + where the product can also bind reversibly to the enzyme. Both the + substrate and product form distinct enzyme complexes, and the catalytic + step itself is reversible. + + The reaction scheme is + + S + E <--> S:E <--> E:P <--> E + P + + where S is the substrate, E is the enzyme, S:E is the enzyme-substrate + complex, E:P is the enzyme-product complex, and P is the product. + + Parameters + ---------- + name : str, default='michaelis_menten_reverse_binding' + Name identifier for this mechanism instance. + mechanism_type : str, default='catalysis' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('catalysis'). + + See Also + -------- + MichaelisMenten : Standard Michaelis-Menten with irreversible catalysis. + MichaelisMentenCopy : Michaelis-Menten preserving substrate. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates three mass-action reactions: + + 1. Reversible substrate binding: S + E <--> S:E (rates 'kb1' and 'ku1') + 2. Reversible product binding: P + E <--> E:P (rates 'kb2' and 'ku2') + 3. Reversible catalysis: S:E <--> E:P (rates 'kcat' and 'kcat_rev') + + Common applications include: + + - Reversible enzymatic reactions near equilibrium + - Bidirectional metabolic pathways + - Reactions where product inhibition is significant + - Detailed kinetic models requiring thermodynamic consistency + + Required parameters for this mechanism: + + - 'kb1' : Forward binding rate for substrate-enzyme association + - 'ku1' : Reverse unbinding rate for substrate-enzyme dissociation + - 'kb2' : Forward binding rate for product-enzyme association + - 'ku2' : Reverse unbinding rate for product-enzyme dissociation + - 'kcat' : Forward catalytic rate constant (S:E --> E:P) + - 'kcat_rev' : Reverse catalytic rate constant (E:P --> S:E) + + This mechanism is particularly useful when modeling reactions close to + equilibrium where the reverse reaction and product binding cannot be + neglected. + + Examples + -------- + Model a reversible enzymatic conversion: + + >>> enzyme = bcp.Species('E', material_type='protein') + >>> substrate = bcp.Species('S') + >>> product = bcp.Species('P') + >>> comp = bcp.Enzyme( + ... enzyme, substrates=[substrate], products=[product], + ... mechanisms={'catalysis': bcp.MichaelisMentenReversible()}, + ... parameters={ + ... 'kb1': 2.0, 'ku1': 0.5, + ... 'kb2': 1.5, 'ku2': 0.3, + ... 'kcat': 1.0, 'kcat_rev': 0.4 + ... } + ... ) + >>> mixture = bcp.Mixture(components=[comp]) + >>> mixture.compile_crn() + Species = protein_E, S, P, complex_S_protein_E_, complex_P_protein_E_ + Reactions = [ + S+protein[E] <--> complex[S:protein[E]] + P+protein[E] <--> complex[P:protein[E]] + complex[S:protein[E]] <--> complex[P:protein[E]] + ] - In the Copy RXN version, the substrate is not Consumed - sub + enz <--> sub:enz <--> enz:prod <--> enz + prod """ def __init__( @@ -206,18 +717,45 @@ def __init__( name='michaelis_menten_reverse_binding', mechanism_type='catalysis', ): - """Initializes a MichaelisMentenReversible instance. - - :param name: name of the Mechanism, default: - 'michaelis_menten_reverse_binding' - :param mechanism_type: type of the Mechanism, default: 'catalysis' - :param keywords: - """ Mechanism.__init__(self, name, mechanism_type) def update_species( self, enzyme, substrate, product, complex=None, complex2=None ): + """Generate species for reversible Michaelis-Menten kinetics. + + Creates the species involved in reversible Michaelis-Menten enzyme + kinetics: enzyme, substrate, product, enzyme-substrate complex, and + enzyme-product complex. + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the reaction. + substrate : Species + The substrate species. + product : Species + The product species. + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + complex2 : Species, optional + Pre-specified enzyme-product complex. If None, automatically + creates a Complex([product, enzyme]). + + Returns + ------- + list of Species + List containing [enzyme, substrate, product, complex1, + complex2] where complex1 is S:E and complex2 is E:P. + + Notes + ----- + Both complexes are automatically generated if not explicitly + provided. The enzyme-substrate complex contains [substrate, enzyme] + and the enzyme-product complex contains [product, enzyme]. + + """ if complex is None: complex1 = Complex([substrate, enzyme]) else: @@ -241,6 +779,67 @@ def update_reactions( ku=None, kcat=None, ): + """Generate reactions for reversible Michaelis-Menten kinetics. + + Creates three mass-action reactions implementing reversible + Michaelis-Menten enzyme kinetics with product binding: substrate + binding, product binding, and reversible catalysis. + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the reaction. + substrate : Species + The substrate species. + product : Species + The product species. + component : Component, optional + Component containing parameter values. Required if kb, ku, or + kcat are not provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + complex2 : Species, optional + Pre-specified enzyme-product complex. If None, automatically + creates a Complex([product, enzyme]). + kb : tuple of (float or Parameter), optional + Tuple of (kb1, kb2) binding rate constants. If None, kb1 and + kb2 retrieved separately from component parameters. + ku : tuple of (float or Parameter), optional + Tuple of (ku1, ku2) unbinding rate constants. If None, ku1 and + ku2 retrieved separately from component parameters. + kcat : tuple of (float or Parameter), optional + Tuple of (kcat, kcat_rev) catalytic rate constants. If None, + kcat and kcat_rev retrieved separately from component + parameters. + + Returns + ------- + list of Reaction + List containing three reactions: [substrate_binding_reaction, + product_binding_reaction, catalysis_reaction]. + + Raises + ------ + ValueError + If component is None and any of kb, ku, or kcat is not + provided. + + Notes + ----- + The mechanism generates the following reactions: + + 1. S + E <--> S:E (binding, rates 'kb1' and 'ku1') + 2. P + E <--> E:P (binding, rates 'kb2' and 'ku2') + 3. S:E <--> E:P (catalysis, rates 'kcat' and 'kcat_rev') + + When providing parameters directly (not via component), kb, ku, and + kcat should be tuples of two values each. + + """ # Get parameters if part_id is None and component is not None: part_id = component.name @@ -311,21 +910,127 @@ def update_reactions( class MichaelisMentenCopy(Mechanism): - """In the Copy RXN version, the substrate is not consumed. + """Michaelis-Menten kinetics with substrate preservation. + + A 'copy' mechanism implementing Michaelis-Menten enzyme kinetics where + the substrate is not consumed during the reaction. Instead, the + substrate acts as a template that is copied or read, producing a + product while preserving the original substrate. + + The reaction scheme is + + S + E <--> S:E --> S + E + P + + where S is the substrate (template), E is the enzyme, S:E is the + enzyme-substrate complex, and P is the product. + + Parameters + ---------- + name : str, default='michaelis_menten_copy' + Name identifier for this mechanism instance. + mechanism_type : str, default='copy' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('copy'). + + See Also + -------- + MichaelisMenten : Standard Michaelis-Menten consuming substrate. + BasicProduction : Simpler production without complex formation. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates two mass-action reactions: + + 1. Reversible binding: S + E <--> S:E (rates 'kb' and 'ku') + 2. Catalytic copying: S:E --> S + E + P (rate 'kcat') + + Common applications include: + + - Gene transcription (DNA template produces RNA) + - Translation (mRNA template produces protein) + - DNA replication + - Any process where a template is read without being consumed + + Required parameters for this mechanism: + + - 'kb' : Binding rate constant for enzyme-substrate association + - 'ku' : Unbinding rate constant for enzyme-substrate dissociation + - 'kcat' : Catalytic rate constant for product formation + + The key difference from standard Michaelis-Menten is that the substrate + appears on both sides of the catalytic step, making it a true copying + or templating mechanism rather than a conversion. + + Examples + -------- + Model translation with component: + + >>> mrna = bcp.Species('mRNA') + >>> ribosome = bcp.Species('Ribo') + >>> protein = bcp.species('GFP') + >>> comp = bcp.Enzyme( + ... ribosome, substrates=[mrna], products=[protein], + ... parameters={'kb': 2.0, 'ku': 0.2, 'kcat': 0.1} + ... ) + >>> mixture = bcp.Mixture( + ... components=[comp], + ... mechanisms={'catalysis': bcp.MichaelisMentenCopy()}, + ... ) + >>> mixture.compile_crn() + Species = Ribo, mRNA, complex_Ribo_mRNA_, GFP + Reactions = [ + mRNA+Ribo <--> complex[Ribo:mRNA] + complex[Ribo:mRNA] --> mRNA+GFP+Ribo + ] - substrate + Enz <--> substrate:Enz --> substrate + Enz + product """ def __init__(self, name='michaelis_menten_copy', mechanism_type='copy'): - """Initializes a MichaelisMentenCopy instance. - - :param name: name of the Mechanism, default: 'michaelis_menten_copy' - :param mechanism_type: type of the Mechanism, default: 'copy' - :param keywords: - """ Mechanism.__init__(self, name, mechanism_type) def update_species(self, enzyme, substrate, complex=None, product=None): + """Generate species for copy-type Michaelis-Menten kinetics. + + Creates the species involved in copy-type Michaelis-Menten enzyme + kinetics: enzyme, substrate (template), enzyme-substrate complex, + and optionally the product(s). + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the copying reaction. + substrate : Species + The substrate (template) species that is copied but not + consumed. + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + product : Species or list of Species, optional + The product species or list of products. If None, only enzyme, + substrate, and complex are returned. + + Returns + ------- + list of Species + List containing [enzyme, substrate, complex] if product is + None. If product is provided, returns [enzyme, substrate, + complex, product] for single product or [enzyme, substrate, + complex] + product for list of products. + + Notes + ----- + This method can handle multiple products by accepting product as a + list. This is useful for modeling processes like transcription + where multiple transcript copies may be produced. + + """ if complex is None: complexS = Complex([substrate, enzyme]) else: @@ -350,6 +1055,64 @@ def update_reactions( ku=None, kcat=None, ): + """Generate reactions for copy-type Michaelis-Menten kinetics. + + Creates two mass-action reactions implementing copy-type + Michaelis-Menten enzyme kinetics: reversible enzyme-substrate + binding and catalytic copying that preserves the substrate. + + Parameters + ---------- + enzyme : Species + The enzyme species that catalyzes the copying reaction. + substrate : Species + The substrate (template) species that is copied but not + consumed. + product : Species + The product species. + component : Component, optional + Component containing parameter values. Required if kb, ku, or + kcat are not provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Species, optional + Pre-specified enzyme-substrate complex. If None, automatically + creates a Complex([substrate, enzyme]). + kb : Parameter or float, optional + Forward binding rate constant. If None, retrieved from + component parameters. + ku : Parameter or float, optional + Reverse unbinding rate constant. If None, retrieved from + component parameters. + kcat : Parameter or float, optional + Catalytic rate constant. If None, retrieved from component + parameters. + + Returns + ------- + list of Reaction + List containing two reactions: + [binding_reaction, catalysis_reaction]. + + Raises + ------ + ValueError + If component is None and any of kb, ku, or kcat is not + provided. + + Notes + ----- + The mechanism generates the following reactions: + + 1. S + E <--> S:E (binding, rates 'kb' and 'ku') + 2. S:E --> S + E + P (copying, rate 'kcat') + + The key feature is that the substrate appears on both sides of the + catalytic reaction, ensuring it is not consumed. This makes the + reaction a true template-based copying mechanism. + + """ if complex is None: complexS = Complex([substrate, enzyme]) else: diff --git a/biocrnpyler/mechanisms/global_mechanisms.py b/biocrnpyler/mechanisms/global_mechanisms.py index 8fba4e81..f4396497 100644 --- a/biocrnpyler/mechanisms/global_mechanisms.py +++ b/biocrnpyler/mechanisms/global_mechanisms.py @@ -44,23 +44,103 @@ class GlobalMechanism(Mechanism): - """Global mechanisms are a lot like mechanisms. - - They are called only by mixtures on a list of all species have been - generated by components. Global mechanisms are meant to work as universal - mechanisms that function on each species or all species of some material - type or with some attribute. Global mechanisms may only act on one species - at a time. - - In order to decide which species a global mechanism acts upon, the - filter_dict is used. - - An example of a global mechanism is degradation via dilution which is - demonstrated in the Tests folder. - - GlobalMechanisms should be used cautiously or avoided alltogether - the - order in which they are called may have to be carefully user-defined in - the subclasses of Mixture in order to ensure expected behavior. + """Base class for global mechanisms that act on all species in a mixture. + + Global mechanisms are applied by mixtures to all species after components + have generated their species. Unlike regular mechanisms that act on + specific component interactions, global mechanisms function universally on + species based on their properties (material type, attributes, or name). + + Global mechanisms use a filter dictionary to determine which species they + act upon. For each species, the mechanism checks the species's material + type, attributes, and name against the filter dictionary. If a match + returns True, the mechanism acts on that species; if False, it does not. + + Parameters + ---------- + name : str + Name identifier for this mechanism instance. + mechanism_type : str, default='' + Type classification of this mechanism. + filter_dict : dict, optional + Dictionary mapping species properties (material_type, attributes, + or name) to boolean values. True means the mechanism acts on + species with that property, False means it does not. If None, + an empty dictionary is used. + default_on : bool, default=False + Determines behavior when a species is not found in filter_dict. + If True, the mechanism acts on unfiltered species. If False, it + does not. Also used as the default when filter conflicts occur. + recursive_species_filtering : bool, default=False + Determines how ComplexSpecies are filtered. If True, filtering + is based on all subspecies recursively. If False, filtering acts + only on the ComplexSpecies itself. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification of this mechanism. + filter_dict : dict + Dictionary for filtering species. + default_on : bool + Default behavior for unfiltered species. + recursive_species_filtering : bool + Whether to filter ComplexSpecies recursively. + + See Also + -------- + Dilution : Global mechanism for species dilution. + Degradation_mRNA_MM : Global mRNA degradation mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + GlobalMechanisms should be used cautiously as the order in which they + are called may affect the final CRN. The calling order may need to be + carefully defined in Mixture subclasses to ensure expected behavior. + + Filter dictionary usage: + + - Keys can be material_type, attribute names, or species names + - Values are boolean (True = apply mechanism, False = do not apply) + - When conflicts occur, a warning is issued and default_on is used + - Attributes take precedence over material_type when both are present + + All parameters required by the global mechanism must be present in the + Mixture's parameter dictionary. Global mechanisms are assumed to take + a single species as input. + + Examples + -------- + Create a custom global degradation mechanism: + + >>> class CustomDegradation(bcp.GlobalMechanism): + ... def update_reactions(self, s, mixture): + ... kdeg = self.get_parameter(s, 'kdeg', mixture) + ... return [bcp.Reaction.from_massaction( + ... inputs=[s], outputs=[], k_forward=kdeg + ... )] + + Use with a filter to degrade only proteins: + + >>> mech = CustomDegradation( + ... name='protein_degradation', + ... mechanism_type='degradation', + ... filter_dict={'protein': True}, + ... default_on=False + ... ) + >>> mixture = bcp.Mixture( + ... components=[bcp.Protein('A'), bcp.RNA('B')], + ... mechanisms={'degradation': mech}, + ... parameters={'kdeg': 0.01} + ... ) + >>> mixture.compile_crn() + Species = protein_A, rna_B + Reactions = [ + protein[A] --> + ] """ @@ -72,36 +152,6 @@ def __init__( default_on: bool = False, recursive_species_filtering: bool = False, ): - """Creates a GlobalMechanisms instance. - - If the species's name, material type, and attributes are all not in - the filter_dict, the GlobalMechanism will be called if default_on == - True and not called if default_on == False. - - :param name: name of the GlobalMechanism - :param mechanism_type: - :param filter_dict: filter_dict[species.material_type / - species.attributes] = True / False. For each species, its - material type or attributes are sent through the filter_dict. If - True is returned, the GlobalMechanism will act on the species. If - False is returned, the the GlobalMechanism will not be called. If - filter_dict[attribute] is different from - filter_dict[material_type], filter_dict[attribute] takes - precedent. If multiple filter_dict[attribute] contradict for - different attributes, an error is raised. Note that the above - filtering is done automatically. Any parameters needed by the - global mechanism must be in the Mixture's parameter - dictionary. These methods are assumed to take a single species as - input. :param default_on: what to do if a species doesn't come up - in the filter dict. Also used for as the default if there is a - filterdict conflict :param recursive_species_filtering: keyword - determines how the material_type and name of ComplexSpecies is - defined. If True: the filter based upon all subspecies.type and - name recursively going through all ComplexSpecies. If False: the - filter dict will act only on the ComplexSpecies. By default, this - is False. - - """ if filter_dict is None: self.filter_dict = {} else: @@ -112,10 +162,33 @@ def __init__( Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) def apply_filter(self, s: Species): - """Determine if global mechanism acts on a species. + """Determine if the global mechanism should act on a species. + + Checks the species's material_type, attributes, and name against the + filter dictionary to decide if the mechanism should be applied. + + Parameters + ---------- + s : Species + The species to check against the filter dictionary. + + Returns + ------- + bool + True if the mechanism should act on this species, False + otherwise. + + Notes + ----- + The filtering logic follows this hierarchy: + + 1. Checks all attributes and material_type of the species (and + subspecies if recursive_species_filtering is True) + 2. If any match is found in filter_dict, uses that boolean value + 3. If conflicts occur (different attributes give different results), + issues a warning and uses default_on + 4. If no match is found, uses default_on - :param s: Species - :return: """ fd = self.filter_dict use_mechanism = None @@ -143,6 +216,28 @@ def apply_filter(self, s: Species): def update_species_global( self, species_list: List[Species], mixture, compartment=None ): + """Apply mechanism's update_species to filtered species in a list. + + Iterates through all species in the list, applies the filter to + determine which species the mechanism acts upon, and calls + update_species for each applicable species. + + Parameters + ---------- + species_list : list of Species + List of all species to potentially act upon. + mixture : Mixture + The mixture containing parameters and other context. + compartment : Compartment, optional + If provided, assigns this compartment to any new species that + have the default compartment. + + Returns + ------- + list of Species + List of all new species generated by the mechanism. + + """ new_species = [] for s in species_list: use_mechanism = self.apply_filter(s) @@ -160,6 +255,28 @@ def update_species_global( def update_reactions_global( self, species_list: List[Species], mixture, compartment=None ): + """Apply mechanism's `update_reactions` to filtered species in a list. + + Iterates through all species in the list, applies the filter to + determine which species the mechanism acts upon, and calls + `update_reactions` for each applicable species. + + Parameters + ---------- + species_list : list of Species + List of all species to potentially act upon. + mixture : Mixture + The mixture containing parameters and other context. + compartment : Compartment, optional + If provided, assigns this compartment to species that have the + default compartment before generating reactions. + + Returns + ------- + list of Reaction + List of all new reactions generated by the mechanism. + + """ new_reactions = [] for s in species_list: use_mechanism = self.apply_filter(s) @@ -170,6 +287,30 @@ def update_reactions_global( return new_reactions def get_parameter(self, species, param_name, mixture): + """Retrieve a parameter value from the mixture for a given species. + + Parameters + ---------- + species : Species + The species for which to retrieve the parameter. Used as the + part_id for parameter lookup. + param_name : str + Name of the parameter to retrieve. + mixture : Mixture + The mixture containing the parameters. + + Returns + ------- + Parameter or float + The parameter value retrieved from the mixture. + + Raises + ------ + ValueError + If no parameter matching the (mechanism, species, param_name) + combination can be found. + + """ param = mixture.get_parameter( mechanism=self, part_id=repr(species), param_name=param_name ) @@ -184,33 +325,158 @@ def get_parameter(self, species, param_name, mixture): return param def update_species(self, s: Species, mixture): - """Updat specifies for a global mechanism. - - All global mechanisms must use update_species functions with these - inputs. - - :param s: Species instance - :return: + """Generate new species for a global mechanism acting on one species. + + This is a template method that should be overridden by subclasses + to define the species generated by the mechanism. + + Parameters + ---------- + s : Species + The species that the mechanism is acting upon. + mixture : Mixture + The mixture containing parameters and other context. + + Returns + ------- + list of Species + List of new species generated by the mechanism. Default + implementation returns an empty list. + + Notes + ----- + All GlobalMechanism subclasses should implement this method if they + need to generate new species (e.g., enzyme-substrate complexes for + degradation mechanisms). """ return [] def update_reactions(self, s, mixture): - """Updat reactions for a global mechanism. - - All global mechanisms must use update_reactions functions with these - inputs. - - :param s: - :param mixture: - :return: + """Generate reactions for a global mechanism acting on one species. + + This is a template method that should be overridden by subclasses + to define the reactions generated by the mechanism. + + Parameters + ---------- + s : Species + The species that the mechanism is acting upon. + mixture : Mixture + The mixture containing parameters and other context. + + Returns + ------- + list of Reaction + List of reactions generated by the mechanism. Default + implementation returns an empty list. + + Notes + ----- + All GlobalMechanism subclasses should implement this method to + define the reactions the mechanism produces (e.g., degradation + reactions, dilution reactions, etc.). """ return [] class Dilution(GlobalMechanism): - """A global mechanism to represent dilution.""" + """Global mechanism for species dilution or degradation. + + A 'dilution' mechanism that removes species from the system at a rate + proportional to their concentration. This models dilution due to cell + growth, continuous flow in a bioreactor, or general degradation + processes. + + The dilution reaction for each species is + + S --> ∅ + + where S is any species and the rate is determined by 'kdil'. + + Parameters + ---------- + name : str, default='global_degradation_via_dilution' + Name identifier for this mechanism instance. + mechanism_type : str, default='dilution' + Type classification of this mechanism. + filter_dict : dict, optional + Dictionary for filtering which species undergo dilution. If None, + all species are affected based on default_on. + default_on : bool, default=True + If True, dilution applies to all species not explicitly filtered + out. If False, dilution applies only to explicitly filtered species. + recursive_species_filtering : bool, default=True + If True, filters based on all subspecies within ComplexSpecies. If + False, filters only the ComplexSpecies itself. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('dilution'). + filter_dict : dict + Dictionary for filtering species. + default_on : bool + Default behavior for unfiltered species. + recursive_species_filtering : bool + Whether to filter ComplexSpecies recursively. + + See Also + -------- + AntiDilutionConstitutiveCreation : Counter-mechanism for constant + concentration. + GlobalMechanism : Base class for global mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction for + each species that passes the filter, with rate constant 'kdil'. By + default, it applies to all species (default_on=True) unless specific + species are excluded via the filter_dict. + + Common applications include: + + - Modeling cell growth and dilution in batch cultures + - Continuous flow bioreactor systems + - Simplified degradation for all cellular components + - Washout effects in chemostats + + Required parameters for this mechanism: + + - 'kdil' : Dilution rate constant (per species if needed) + + The mechanism can be selectively applied using filter_dict. For + example, to exclude specific species from dilution, set + filter_dict={'notdiluted': False} and tag those species with the + 'notdiluted' attribute. + + Examples + -------- + Apply dilution to all species in a mixture: + + >>> dilution_mech = bcp.Dilution(default_on=True) + >>> mixture = bcp.Mixture( + ... components=[bcp.Protein('A'), bcp.Protein('B')], + ... mechanisms={'dilution': dilution_mech}, + ... parameters={'kdil': 0.01} + ... ) + + Apply dilution only to proteins, excluding DNA: + + >>> dilution_mech = bcp.Dilution( + ... filter_dict={'protein': True, 'dna': False}, + ... default_on=False + ... ) + >>> mixture = bcp.Mixture( + ... components=[bcp.Protein('P'), bcp.DNA('gene')], + ... mechanisms={'dilution': dilution_mech}, + ... parameters={'kdil': 0.01} + ... ) + + """ def __init__( self, @@ -230,6 +496,24 @@ def __init__( ) def update_reactions(self, s: Species, mixture): + """Generate dilution reaction for a single species. + + Creates an irreversible mass-action reaction that removes the + species from the system at rate 'kdil'. + + Parameters + ---------- + s : Species + The species undergoing dilution. + mixture : Mixture + The mixture containing the 'kdil' parameter. + + Returns + ------- + list of Reaction + List containing a single reaction: S --> ∅ with rate 'kdil'. + + """ k_dil = self.get_parameter(s, 'kdil', mixture) rxn = Reaction.from_massaction( inputs=[s], outputs=[], k_forward=k_dil @@ -237,11 +521,103 @@ def update_reactions(self, s: Species, mixture): return [rxn] -class AnitDilutionConstiutiveCreation(GlobalMechanism): - """Constitutively create certain species at the rate of dilution. - - Useful for keeping machinery species like ribosomes and polymerases at a - constant concentration. +class AntiDilutionConstitutiveCreation(GlobalMechanism): + """Global mechanism for constitutive species creation to counter dilution. + + A 'dilution' mechanism that constitutively creates species at a constant + rate to maintain their concentration despite dilution. This is useful for + modeling cellular machinery (ribosomes, polymerases, etc.) that is + maintained at approximately constant levels through homeostatic + mechanisms. + + The production reaction for each species is + + ∅ --> S + + where S is any species and the rate is determined by 'kdil' (matching + the dilution rate to maintain steady state). + + Parameters + ---------- + name : str, default='anti_dilution_constiuitive_creation' + Name identifier for this mechanism instance. + material_type : str, default='dilution' + Type classification of this mechanism (used as mechanism_type). + filter_dict : dict, optional + Dictionary for filtering which species are constitutively created. + If None, all species are affected based on default_on. + default_on : bool, default=True + If True, creation applies to all species not explicitly filtered + out. If False, creation applies only to explicitly filtered species. + recursive_species_filtering : bool, default=True + If True, filters based on all subspecies within ComplexSpecies. If + False, filters only the ComplexSpecies itself. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('dilution'). + filter_dict : dict + Dictionary for filtering species. + default_on : bool + Default behavior for unfiltered species. + recursive_species_filtering : bool + Whether to filter ComplexSpecies recursively. + + See Also + -------- + Dilution : Dilution mechanism this counteracts. + GlobalMechanism : Base class for global mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction for + each species that passes the filter, with rate constant 'kdil'. It is + typically used in conjunction with the Dilution mechanism to maintain + steady-state concentrations of cellular machinery. + + Common applications include: + + - Maintaining ribosome and polymerase concentrations in models + - Homeostatic regulation of protein levels + - Buffered species in cell-free systems + - Representing constitutive expression of essential genes + + Required parameters for this mechanism: + + - 'kdil' : Creation rate constant (typically equal to dilution rate) + + When used with Dilution on the same species with the same 'kdil' value, + the species concentration remains constant (steady state). + + Examples + -------- + Maintain ribosome concentration despite dilution: + + >>> ribosome = bcp.Protein('ribosome') + >>> dilution = bcp.Dilution(default_on=True) + >>> creation = bcp.AntiDilutionConstitutiveCreation( + ... filter_dict={'ribosome': True}, + ... default_on=False + ... ) + >>> mixture = bcp.Mixture( + ... components=[ribosome], + ... mechanisms={'dilution': dilution, 'creation': creation}, + ... parameters={'kdil': 0.01} + ... ) + + Maintain all machinery species at constant levels: + + >>> machinery_species = [ + ... bcp.Protein('ribosome', attributes=['machinery']), + ... bcp.Protein('RNAP', attributes=['machinery']) + ... ] + >>> creation = bcp.AntiDilutionConstitutiveCreation( + ... filter_dict={'machinery': True}, + ... default_on=False + ... ) """ @@ -263,6 +639,24 @@ def __init__( ) def update_reactions(self, s, mixture): + """Generate constitutive creation reaction for a single species. + + Creates an irreversible mass-action reaction that produces the + species at rate 'kdil' to counteract dilution. + + Parameters + ---------- + s : Species + The species being constitutively created. + mixture : Mixture + The mixture containing the 'kdil' parameter. + + Returns + ------- + list of Reaction + List containing a single reaction: ∅ --> S with rate 'kdil'. + + """ k_dil = self.get_parameter(s, 'kdil', mixture) rxn = Reaction.from_massaction( inputs=[], outputs=[s], k_forward=k_dil @@ -271,13 +665,109 @@ def update_reactions(self, s, mixture): class Degradation_mRNA_MM(GlobalMechanism, MichaelisMenten): - """Michaelis Menten mRNA Degradation by Endonucleases. + """Michaelis-Menten mRNA degradation by endonucleases. + + A 'rna_degradation' mechanism that uses Michaelis-Menten kinetics to + model the enzymatic degradation of mRNA by endonucleases. All species + with material_type 'rna' are degraded, including those within + ComplexSpecies. + + The degradation reaction scheme is mRNA + Endo <--> mRNA:Endo --> Endo - All species of type "rna" are degraded by this mechanisms, including those - inside of a ComplexSpecies. ComplexSpecies are seperated by this process, - including embedded ComplexSpecies. OrderedPolymerSpecies are ignored. + where mRNA is any RNA species and Endo is the endonuclease. + + Parameters + ---------- + nuclease : Species + The endonuclease species that degrades mRNA. + name : str, default='rna_degradation_mm' + Name identifier for this mechanism instance. + mechanism_type : str, default='rna_degradation' + Type classification of this mechanism. + default_on : bool, default=False + If True, mechanism acts on all species not filtered. If False, only + acts on filtered species. + recursive_species_filtering : bool, default=True + If True, searches for RNA within ComplexSpecies recursively. If + False, only acts on top-level species. + filter_dict : dict, optional + Dictionary for filtering species. Default is {'rna': True, + 'notdegradable': False} to degrade RNA but not species marked as + not degradable. + **kwargs + Additional keyword arguments passed to parent classes. + + Attributes + ---------- + nuclease : Species + The endonuclease species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('rna_degradation'). + filter_dict : dict + Dictionary for filtering species. + default_on : bool + Default behavior for unfiltered species. + recursive_species_filtering : bool + Whether to filter ComplexSpecies recursively. + + See Also + -------- + MichaelisMenten : Base enzyme kinetics mechanism. + Deg_Tagged_Degradation : Targeted protein degradation. + GlobalMechanism : Base class for global mechanisms. + + Notes + ----- + This mechanism handles three cases: + + 1. Pure RNA species: Degraded completely (RNA --> ∅) + 2. RNA in ComplexSpecies: Complex is broken apart, RNA is degraded, + non-RNA components are released + 3. OrderedPolymerSpecies: Not affected by this mechanism + + The mechanism generates Michaelis-Menten reactions with three rate + constants per species. ComplexSpecies containing RNA are separated + during degradation, including embedded ComplexSpecies. However, + OrderedPolymerSpecies (like DNA or assembled proteins) are ignored. + + Required parameters for this mechanism: + + - 'kdeg' : Catalytic rate constant for RNA degradation + - 'kb' : Forward binding rate for nuclease-RNA association + - 'ku' : Reverse unbinding rate for nuclease-RNA dissociation + + The default filter_dict applies degradation to all 'rna' species but + excludes any species with the 'notdegradable' attribute, allowing + fine-grained control over which RNA species are degraded. + + Examples + -------- + Model global mRNA degradation in a cell-free system: + + >>> rnase = bcp.Protein('RNase') + >>> mrna = bcp.RNA('mRNA') + >>> degradation = bcp.Degradation_mRNA_MM(nuclease=rnase.species) + >>> mixture = bcp.Mixture( + ... components=[rnase, mrna], + ... mechanisms={'rna_degradation': degradation}, + ... parameters={'kdeg': 0.1, 'kb': 1.0, 'ku': 0.5} + ... ) + + Protect specific RNAs from degradation: + + >>> rnase = bcp.Protein('RNase') + >>> stable_rna = bcp.RNA('stable', attributes=['notdegradable']) + >>> unstable_rna = bcp.RNA('unstable') + >>> degradation = bcp.Degradation_mRNA_MM(nuclease=rnase.species) + >>> mixture = bcp.Mixture( + ... components=[rnase, stable_rna, unstable_rna], + ... mechanisms={'rna_degradation': degradation}, + ... parameters={'kdeg': 0.1, 'kb': 1.0, 'ku': 0.5} + ... ) """ @@ -289,7 +779,7 @@ def __init__( default_on=False, recursive_species_filtering=True, filter_dict=None, - **keywords, + **kwargs, ): if isinstance(nuclease, Species): self.nuclease = nuclease @@ -312,6 +802,36 @@ def __init__( ) def update_species(self, s, mixture): + """Generate species for mRNA degradation reactions. + + Creates enzyme-substrate complexes needed for Michaelis-Menten + degradation kinetics. Handles RNA in ComplexSpecies by identifying + non-RNA components that will be released. + + Parameters + ---------- + s : Species + The species to check for RNA degradation. + mixture : Mixture + The mixture containing parameters. + + Returns + ------- + list of Species + List of new species (enzyme-substrate complexes) generated for + degradation reactions. Empty list if species should not be + degraded. + + Notes + ----- + Behavior depends on species type: + + - Pure RNA species: Creates nuclease:RNA complex + - ComplexSpecies containing RNA: Creates nuclease:complex complex, + identifies non-RNA products to be released + - OrderedPolymerSpecies: Returns empty list (not degraded) + + """ species = [] # Check if rna species are inside a ComplexSpecies. @@ -348,6 +868,36 @@ def update_species(self, s, mixture): return species def update_reactions(self, s, mixture): + """Generate Michaelis-Menten degradation reactions for mRNA. + + Creates two mass-action reactions implementing Michaelis-Menten + kinetics for RNA degradation: reversible binding and irreversible + catalysis. + + Parameters + ---------- + s : Species + The species to check for RNA degradation. + mixture : Mixture + The mixture containing parameters 'kdeg', 'kb', and 'ku'. + + Returns + ------- + list of Reaction + List of reactions for RNA degradation. Empty list if species + should not be degraded. + + Notes + ----- + Generates standard Michaelis-Menten reactions: + + 1. RNA + nuclease <--> RNA:nuclease (rates 'kb' and 'ku') + 2. RNA:nuclease --> nuclease (rate 'kdeg') + + For ComplexSpecies containing RNA, non-RNA components are released + in the catalytic step instead of being degraded. + + """ reactions = [] # Check if rna species are inside a ComplexSpecies. @@ -406,31 +956,135 @@ def update_reactions(self, s, mixture): class Deg_Tagged_Degradation(GlobalMechanism, MichaelisMenten): - """Michaelis Menten Degradation of deg-tagged proteins by degredase. - - Species_degtagged + degredase <--> Species_degtagged:degredase - --> degredase - - All species with the attribute degtagged and material_type protein are - degraded. The method is not recursive. + """Michaelis-Menten degradation of deg-tagged proteins by degradase. + + A 'degradation' mechanism that uses Michaelis-Menten kinetics to model + the targeted enzymatic degradation of proteins tagged for degradation + (e.g., via degron sequences). Only species with a specific degradation + tag attribute and material_type 'protein' are degraded. + + The degradation reaction scheme is + + Protein_degtagged + degradase <--> Protein_degtagged:degradase + --> degradase + + where Protein_degtagged is any protein with the degradation tag. + + Parameters + ---------- + degradase : Species + The degradase enzyme species that degrades tagged proteins. + deg_tag : str, default='degtagged' + The attribute name used to identify proteins tagged for + degradation. + name : str, default='deg_tagged_degradation' + Name identifier for this mechanism instance. + mechanism_type : str, default='degradation' + Type classification of this mechanism. + filter_dict : dict, optional + Dictionary for filtering species. Default is {deg_tag: True} to + degrade only species with the degradation tag. + recursive_species_filtering : bool, default=False + If True, searches within ComplexSpecies recursively. If False, + only acts on top-level species. + default_on : bool, default=False + If True, mechanism acts on all species not filtered. If False, only + acts on filtered species. + **kwargs + Additional keyword arguments passed to parent classes. + + Attributes + ---------- + degradase : Species + The degradase enzyme species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('degradation'). + filter_dict : dict + Dictionary for filtering species. + default_on : bool + Default behavior for unfiltered species. + recursive_species_filtering : bool + Whether to filter ComplexSpecies recursively. + + See Also + -------- + MichaelisMenten : Base enzyme kinetics mechanism. + Degradation_mRNA_MM : Global mRNA degradation mechanism. + GlobalMechanism : Base class for global mechanisms. + + Notes + ----- + This mechanism implements targeted protein degradation similar to + biological systems like the ubiquitin-proteasome system or degron-mediated + degradation. Unlike global degradation mechanisms, it only affects + proteins explicitly tagged with the specified attribute. + + The mechanism is not recursive by default, meaning it only degrades + proteins directly tagged with the `deg_tag` attribute, not proteins + within ComplexSpecies unless the complex itself is tagged. + + Common applications include: + + - Modeling ssrA-tagged protein degradation + - Implementing synthetic degron systems + - Targeted protein knockdown experiments + - Conditional protein stability control + + Required parameters for this mechanism: + + - 'kdeg' : Catalytic rate constant for protein degradation + - 'kb' : Forward binding rate for degradase-protein association + - 'ku' : Reverse unbinding rate for degradase-protein dissociation + + The `deg_tag` attribute must be added to protein species that should be + degraded. By default, the mechanism looks for the 'degtagged' + attribute but this can be customized via the `deg_tag` parameter. + + Examples + -------- + Model ssrA-tagged protein degradation: + + >>> clpxp = bcp.Protein('ClpXP') + >>> stable_protein = bcp.Protein('stable') + >>> tagged_protein = bcp.Protein('tagged', attributes=['degtagged']) + >>> degradation = bcp.Deg_Tagged_Degradation( + ... degradase=clpxp.species, + ... deg_tag='degtagged' + ... ) + >>> mixture = bcp.Mixture( + ... components=[clpxp, stable_protein, tagged_protein], + ... mechanisms={'degradation': degradation}, + ... parameters={'kdeg': 0.5, 'kb': 1.0, 'ku': 0.1} + ... ) + + Use custom degradation tags: + + >>> proteasome = bcp.Protein('proteasome') + >>> ubiquitinated = bcp.Protein('target', attributes=['ubiquitinated']) + >>> degradation = bcp.Deg_Tagged_Degradation( + ... degradase=proteasome.species, + ... deg_tag='ubiquitinated' + ... ) """ def __init__( self, - degredase, + degradase, deg_tag='degtagged', name='deg_tagged_degradation', mechanism_type='degradation', filter_dict=None, recursive_species_filtering=False, default_on=False, - **keywords, + **kwargs, ): - if isinstance(degredase, Species): - self.degredase = degredase + if isinstance(degradase, Species): + self.degradase = degradase else: - raise ValueError("'degredase' must be a Species.") + raise ValueError("'degradase' must be a Species.") MichaelisMenten.__init__( self=self, name=name, mechanism_type=mechanism_type ) @@ -448,13 +1102,58 @@ def __init__( ) def update_species(self, s, mixture): + """Generate species for deg-tagged protein degradation reactions. + + Creates enzyme-substrate complexes needed for Michaelis-Menten + degradation kinetics of tagged proteins. + + Parameters + ---------- + s : Species + The species to check for degradation tagging. + mixture : Mixture + The mixture containing parameters. + + Returns + ------- + list of Species + List containing the degradase:protein complex species. + + """ species = [] species += MichaelisMenten.update_species( - self, enzyme=self.degredase, substrate=s, product=None + self, enzyme=self.degradase, substrate=s, product=None ) return species def update_reactions(self, s, mixture): + """Generate reactions for deg-tagged protein degradation reactions. + + Creates two mass-action reactions implementing Michaelis-Menten + kinetics for targeted protein degradation: reversible binding and + irreversible catalysis. + + Parameters + ---------- + s : Species + The tagged protein species to be degraded. + mixture : Mixture + The mixture containing parameters 'kdeg', 'kb', and 'ku'. + + Returns + ------- + list of Reaction + List of two reactions for tagged protein degradation: + [binding_reaction, catalysis_reaction]. + + Notes + ----- + Generates standard Michaelis-Menten reactions: + + 1. Protein + degradase <--> Protein:degradase (rates 'kb' and 'ku') + 2. Protein:degradase --> degradase (rate 'kdeg') + + """ kdeg = self.get_parameter(s, 'kdeg', mixture) kb = self.get_parameter(s, 'kb', mixture) ku = self.get_parameter(s, 'ku', mixture) @@ -462,7 +1161,7 @@ def update_reactions(self, s, mixture): rxns = [] rxns += MichaelisMenten.update_reactions( self, - enzyme=self.degredase, + enzyme=self.degradase, substrate=s, product=None, kb=kb, diff --git a/biocrnpyler/mechanisms/integrase.py b/biocrnpyler/mechanisms/integrase.py index 64ad72f5..393d059c 100644 --- a/biocrnpyler/mechanisms/integrase.py +++ b/biocrnpyler/mechanisms/integrase.py @@ -10,23 +10,108 @@ class BasicIntegration(Mechanism): - """Mechanism for the schema DNA1 + DNA2 --> DNA3 + DNA4.""" + """Basic DNA integration mechanism without enzyme involvement. + + An 'integration' mechanism that models the direct recombination or + integration of two DNA molecules to form two new DNA products. This + represents a simplified integration process without explicit enzyme + involvement, useful for modeling the overall effect of site-specific + recombination. + + The integration reaction is given by + + DNA1 + DNA2 --> DNA3 + DNA4 + + where DNA1 and DNA2 are input DNA molecules, and DNA3 and DNA4 are + the recombined product DNA molecules. + + Parameters + ---------- + name : str, default='basic_integration' + Name identifier for this mechanism instance. + mechanism_type : str, default='integration' + Type classification of this mechanism. + **kwargs + Additional keyword arguments (unused, maintained for API + compatibility). + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('integration'). + + See Also + -------- + EnzymeIntegration : Integration with explicit integrase enzyme. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction + representing the overall integration process. It does not model the + detailed molecular mechanism involving integrase binding or + intermediate complexes. + + Common applications include: + + - Simplified models of site-specific recombination + - DNA rearrangement in synthetic biology circuits + - Cassette exchange systems + - Genome editing with minimal mechanistic detail + + Required parameters for this mechanism: + + - 'kint' : Integration rate constant + + The mechanism does not generate new species (returns empty list from + update_species) as it models the direct conversion without + intermediate complexes. + + Examples + -------- + See 'Specialized_Tutorials/Integrase_Examples'. + + """ def __init__( self, name: str = 'basic_integration', mechanism_type: str = 'integration', - **keywords, + **kwargs, ): - """Initializes a BasicIntegration instance. - - :param name: name of the Mechanism, default: basic_integration - :param mechanism_type: type of the Mechanism, default: integration - :param keywords: - """ Mechanism.__init__(self, name, mechanism_type) - def update_species(self, DNA_inputs, DNA_outputs=None, **keywords): + def update_species(self, DNA_inputs, DNA_outputs=None, **kwargs): + """Generate species for basic integration. + + This method returns an empty list as basic integration does not + create intermediate species or complexes. + + Parameters + ---------- + DNA_inputs : list of Species + List of input DNA species to be integrated. + DNA_outputs : list of Species, optional + List of output DNA species after integration. Not used in + species generation. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + Empty list (no new species generated). + + Notes + ----- + This mechanism does not generate intermediate complexes. If + modeling with explicit integrase-DNA complexes is needed, consider + using a binding mechanism in combination with integration, or use + the `EnzymeIntegration` mechanism. + + """ # this doesn't make any species because I use a Binding # mechanism for that maybe if we do the tetramerization # mechanism then this would do something @@ -39,8 +124,49 @@ def update_reactions( component=None, part_id=None, kint=None, - **keywords, + **kwargs, ): + """Generate integration reaction for basic integration. + + Creates a single irreversible mass-action reaction for DNA + integration. + + Parameters + ---------- + DNA_inputs : list of Species + List of input DNA species to be integrated (typically 2). + DNA_outputs : list of Species + List of output DNA species after integration (typically 2). + component : Component, optional + Component containing parameter values. Required if kint is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + kint : Parameter or float, optional + Integration rate constant. If None, retrieved from component + parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single irreversible mass-action reaction: + DNA_inputs --> DNA_outputs. + + Raises + ------ + ValueError + If component is None and kint is not provided. + + Notes + ----- + The reaction follows simple mass-action kinetics with rate + constant 'kint'. All input DNA species are consumed and all output + DNA species are produced simultaneously. + + """ if part_id is None and component is not None: part_id = component.name @@ -59,11 +185,79 @@ def update_reactions( class EnzymeIntegration(Mechanism): - """Enzymatic integrase mechanism. + """Enzyme-catalyzed DNA integration mechanism with integrase. - Mechanism for the schema: + An 'integration' mechanism that models site-specific recombination + catalyzed by integrase enzymes. The mechanism explicitly includes the + integrase as a catalyst that facilitates DNA recombination but is not + consumed in the reaction. This mechanism uses tetrameric integrase + complexes (4 integrase molecules) to catalyze the integration. - integrase + DNA1 + DNA2 --> integrase + DNA3 + DNA4. + The integration reaction is given by + + 4*Int + DNA1 + DNA2 --> 4*Int + DNA3 + DNA4 + + where Int is the integrase enzyme, DNA1 and DNA2 are input DNA + molecules, and DNA3 and DNA4 are the recombined product DNA molecules. + + Parameters + ---------- + name : str, default='enzyme_integration' + Name identifier for this mechanism instance. + mechanism_type : str, default='integration' + Type classification of this mechanism. + integrase : str, default='Int1' + Name of the integrase enzyme. A Species with this name and + material_type 'protein' is created automatically. + **kwargs + Additional keyword arguments (unused, maintained for API + compatibility). + + Attributes + ---------- + integrase : Species + The integrase enzyme species with material_type 'protein'. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('integration'). + + See Also + -------- + BasicIntegration : Integration without explicit enzyme. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models site-specific recombination with explicit + integrase involvement. The integrase acts as a true catalyst, + appearing on both sides of the reaction and not being consumed. + + The mechanism uses 4 integrase molecules to model the tetrameric + integrase complex typically formed during site-specific recombination + (e.g., lambda phage integration, Cre-loxP recombination). This + reflects the biological reality where integrase monomers form + tetramers to catalyze strand exchange. + + Common applications include: + + - Lambda phage integration/excision systems + - Cre-loxP recombination + - FLP-FRT site-specific recombination + - Detailed models of integrase-mediated genome editing + + Required parameters for this mechanism: + + - 'kint' : Integration rate constant + + The stoichiometry of 4 integrase molecules reflects biological + integrase mechanisms where a tetramer is the active form. The + mechanism does not generate intermediate complexes (returns empty list + from update_species). + + Examples + -------- + See 'Specialized_Tutorials/Integrase_Examples'. """ @@ -72,19 +266,42 @@ def __init__( name: str = 'enzyme_integration', mechanism_type: str = 'integration', integrase='Int1', - **keywords, + **kwargs, ): - """Initializes a BasicIntegration instance. - - :param name: name of the Mechanism, default: basic_integration - :param mechanism_type: type of the Mechanism, default: integration - :param keywords: - """ # TODO ZAT: remove unused keywords argument self.integrase = Species(name=integrase, material_type='protein') Mechanism.__init__(self, name, mechanism_type) - def update_species(self, DNA_inputs, DNA_outputs=None, **keywords): + def update_species(self, DNA_inputs, DNA_outputs=None, **kwargs): + """Generate species for enzyme integration. + + This method returns an empty list as enzyme integration does not + create intermediate species or complexes in this implementation. + + Parameters + ---------- + DNA_inputs : list of Species + List of input DNA species to be integrated. + DNA_outputs : list of Species, optional + List of output DNA species after integration. Not used in + species generation. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + Empty list (no new species generated). + + Notes + ----- + This mechanism does not generate intermediate integrase-DNA + complexes. The integrase-DNA binding and tetramerization steps are + implicitly captured in the overall rate constant 'kint'. For more + detailed mechanistic models, consider using explicit binding + mechanisms before integration. + + """ # this doesn't make any species because I use a Binding # mechanism for that maybe if we do the tetramerization # mechanism then this would do something @@ -97,8 +314,56 @@ def update_reactions( component=None, part_id=None, kint=None, - **keywords, + **kwargs, ): + """Generate integration reaction with integrase enzyme. + + Creates a single irreversible mass-action reaction for enzymatic + DNA integration with tetrameric integrase complex. + + Parameters + ---------- + DNA_inputs : list of Species + List of input DNA species to be integrated (typically 2). + DNA_outputs : list of Species + List of output DNA species after integration (typically 2). + component : Component, optional + Component containing parameter values. Required if kint is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + kint : Parameter or float, optional + Integration rate constant. If None, retrieved from component + parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single irreversible mass-action reaction: + 4*integrase + DNA_inputs --> 4*integrase + DNA_outputs. + + Raises + ------ + ValueError + If component is None and kint is not provided. + + Notes + ----- + The reaction includes 4 integrase molecules on both sides, + modeling the tetrameric integrase complex required for + site-specific recombination. The integrase is not consumed (acts + as a true catalyst). + + The stoichiometry reflects biological mechanisms like: + + - Lambda integrase (Int) tetramer formation + - Cre recombinase synaptic complex + - Other integrase family enzymes requiring multimers + + """ if part_id is None and component is not None: part_id = component.name diff --git a/biocrnpyler/mechanisms/metabolite.py b/biocrnpyler/mechanisms/metabolite.py index 18f51cff..4312aec9 100644 --- a/biocrnpyler/mechanisms/metabolite.py +++ b/biocrnpyler/mechanisms/metabolite.py @@ -2,14 +2,150 @@ from ..core.reaction import Reaction -# [precursors] --> [products] using Massaction (None OK) class OneStepPathway(Mechanism): + """Simple one-step metabolic pathway mechanism. + + A 'metabolic_pathway' mechanism that models the conversion of precursor + metabolites to product metabolites in a single irreversible step. This + mechanism can represent metabolic reactions, spontaneous conversions, or + simplified enzymatic pathways where enzyme dynamics are not explicitly + modeled. + + The pathway reaction can be any of the following forms: + + - Precursors --> Products (standard conversion) + - --> Products (creation from nothing) + - Precursors --> (degradation to nothing) + + where precursors and products can be single species or lists of species. + + Parameters + ---------- + name : str, default='one_step_pathway' + Name identifier for this mechanism instance. + mechanism_type : str, default='metabolic_pathway' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('metabolic_pathway'). + + See Also + -------- + BasicCatalysis : Enzymatic catalysis with explicit enzyme. + BasicProduction : Catalytic production mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism generates a single irreversible mass-action reaction + with rate constant 'k'. It provides flexibility to model various types + of metabolic processes: + + - Standard conversion: Multiple precursors convert to multiple + products + - Creation: Products appear spontaneously (precursor=None) + - Degradation: Precursors disappear (product=None) + + The mechanism does not explicitly model enzymes or intermediate + complexes, making it suitable for: + + - Lumped metabolic pathways + - Spontaneous chemical reactions + - Simplified models where enzyme dynamics are negligible + - Material flow in metabolic networks + - Constitutive processes + + Common applications include: + + - Metabolic flux balance models + - Simplified biosynthetic pathways + - Nutrient uptake and consumption + - Waste product formation + - Simple chemical transformations + + Required parameters for this mechanism: + + - 'k' : Forward rate constant for the conversion + + The mechanism supports arbitrary stoichiometries through the use of + lists for precursors and products. Stoichiometry is determined by the + number of times a species appears in the list. + + Examples + -------- + Model a simple metabolic conversion: + + >>> g6p = bcp.Metabolite('g6p', precursors=['glucose'], products=[]) + >>> mixture = bcp.Mixture( + ... components=[g6p], + ... mechanisms={'metabolic_pathway': bcp.OneStepPathway()}, + ... parameters={'k': 0.1} + ... ) + >>> mixture.compile_crn() + + Model constitutive metabolite production: + + >>> metabolite = bcp.Metabolite( + ... 'metabolite', precursors=[None], products=[]) + >>> mixture = bcp.Mixture( + ... components=[metabolite], + ... mechanisms={'metabolic_pathway': bcp.OneStepPathway()}, + ... parameters={'k': 1.0} + ... ) + >>> mixture.compile_crn() + + Model metabolite degradation: + + >>> waste = bcp.Metabolite( + ... 'waste_product', precursors=[], products=[None]) + >>> mixture = bcp.Mixture( + ... components=[waste], + ... mechanisms={'metabolic_pathway': bcp.OneStepPathway()}, + ... parameters={'k': 0.2} + ... ) + >>> mixture.compile_crn() + + """ + def __init__( self, name='one_step_pathway', mechanism_type='metabolic_pathway' ): Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - def update_species(self, precursor, product, **keywords): + def update_species(self, precursor, product, **kwargs): + """Generate species list for metabolic pathway. + + Collects all species involved in the pathway (precursors and + products) into a single list. + + Parameters + ---------- + precursor : Species, list of Species, or None + Precursor species or list of precursor species. If None, the + pathway represents creation from nothing. + product : Species, list of Species, or None + Product species or list of product species. If None, the + pathway represents degradation to nothing. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + Combined list of all precursor and product species. Returns + empty list if both are None. + + Notes + ----- + This method simply aggregates the input species without creating + new species or complexes. The species should already exist in the + system before this mechanism is applied. + + """ species = [] if precursor is not None: species += precursor @@ -24,8 +160,57 @@ def update_reactions( component=None, part_id=None, k=None, - **keywords, + **kwargs, ): + """Generate metabolic pathway reactions. + + Creates a single irreversible mass-action reaction for the + metabolic conversion of precursors to products. + + Parameters + ---------- + precursor : Species, list of Species, or None + Precursor species or list of precursor species. If None, the + reaction represents creation (no inputs). + product : Species, list of Species, or None + Product species or list of product species. If None, the + reaction represents degradation (no outputs). + component : Component, optional + Component containing parameter values. Required if k is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to using + component's parameter lookup without specific part_id. + k : Parameter or float, optional + Forward rate constant for the pathway. If None, retrieved from + component parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single irreversible mass-action reaction: + precursors --> products. + + Raises + ------ + ValueError + If component is None and k is not provided. + + Notes + ----- + The reaction follows mass-action kinetics with rate constant 'k'. + The mechanism supports three modes: + + - Standard: precursors --> products + - Creation: [] --> products (when precursor is None) + - Degradation: precursors --> [] (when product is None) + + Multiple species of the same type in the precursor or product list + determine the stoichiometry of that species in the reaction. + + """ if precursor is None: inputs = [] else: diff --git a/biocrnpyler/mechanisms/signaling.py b/biocrnpyler/mechanisms/signaling.py index fb33d2fd..75f00058 100644 --- a/biocrnpyler/mechanisms/signaling.py +++ b/biocrnpyler/mechanisms/signaling.py @@ -7,23 +7,111 @@ class Membrane_Signaling_Pathway_MM(Mechanism): - """A mechanism to model a two-component system (TCS) membrane sensor. + """Two-component system membrane sensor with Michaelis-Menten kinetics. - Includes the sensing of signal substrate (SigSub) and the phosphorylation - of the response protein (RP) but not include the reporter circuit. - Mechanism follows Michaelis-Menten type reactions. + A 'membrane_sensor' mechanism that models a two-component system (TCS) + for signal transduction across cellular membranes. This mechanism + includes signal substrate sensing, membrane sensor protein activation, + auto-phosphorylation via ATP, and phosphorylation of response proteins, + but does not include downstream reporter circuits. - Mechanism for the activation of the membrane sensor protein (MSP): - SP + SigSub <--> SP:SigSub --> SP* + The mechanism follows a multi-step Michaelis-Menten kinetic scheme with + the following reaction pathway: - Mechanism for the auto-phosphorylation: - SP* + nATP <--> SP*:nATP --> SP**:nADP --> SP** + nADP + 1. Activation of membrane sensor protein (MSP): - Mechanism for the phosphorylation of response protein: - SP** + RP <--> SP**:RP --> SP*:RP* --> SP*+RP* + SP + SigSub <--> SP:SigSub --> SP* - Mechanism for the dephosphorylation of phosphoryled response protein: - RP* --> RP + Pi + 2. Auto-phosphorylation via ATP: + + SP* + nATP <--> SP*:nATP --> SP**:nADP --> SP** + nADP + + 3. Phosphorylation of response protein (RP): + + SP** + RP <--> SP**:RP --> SP*:RP* --> SP* + RP* + + 4. Dephosphorylation of phosphorylated response protein: + + RP* --> RP + Pi + + Parameters + ---------- + name : str, default='two_component_membrane_signaling' + Name identifier for this mechanism instance. + mechanism_type : str, default='membrane_sensor' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('membrane_sensor'). + + See Also + -------- + Mechanism : Base class for all mechanisms. + MichaelisMenten : Enzyme-substrate mechanism with MM kinetics. + + Notes + ----- + This mechanism models bacterial two-component signaling systems, which + are common environmental sensing pathways. The sensor protein spans the + membrane and undergoes conformational changes upon binding external + signals, leading to autophosphorylation and subsequent phosphotransfer + to response proteins that regulate gene expression. + + The mechanism requires the membrane sensor protein to have an ATP + attribute (membrane_sensor_protein.ATP) that specifies the number of + ATP molecules required for autophosphorylation. + + Required parameters for this mechanism: + + - 'kb_sigMS' : Forward binding rate for signal substrate to membrane + sensor protein + - 'ku_sigMS' : Reverse unbinding rate for signal substrate from + membrane sensor protein + - 'kb_autoPhos' : Forward binding rate for ATP to activated membrane + sensor protein + - 'ku_autoPhos' : Reverse unbinding rate for ATP from activated + membrane sensor protein + - 'k_hydro' : ATP hydrolysis rate constant + - 'ku_waste' : Unbinding rate for ADP waste products + - 'kb_phosRP' : Forward binding rate for response protein to + phosphorylated membrane sensor + - 'ku_phosRP' : Reverse unbinding rate for response protein from + phosphorylated membrane sensor + - 'k_phosph' : Phosphotransfer rate constant to response protein + - 'ku_activeRP' : Unbinding rate for activated response protein + - 'ku_dephos' : Dephosphorylation rate constant for phosphorylated + response protein + + Examples + -------- + Create a two-component signaling system with default parameters: + + >>> response = bcp.Protein(name='OmpR') + >>> sensor = bcp.MembraneSensor( + ... membrane_sensor_protein='EnvZ', + ... response_protein=response.species, + ... assigned_substrate='Phosphate', + ... signal_substrate='Osmolarity', + ... ATP=2 + ... ) + >>> mechanism = bcp.Membrane_Signaling_Pathway_MM() + >>> mixture = bcp.Mixture( + ... components=[sensor, response], + ... mechanisms={'membrane_sensor': mechanism}, + ... parameters={ + ... 'kb_sigMS': 1.0, 'ku_sigMS': 0.1, + ... 'kb_autoPhos': 1.0, 'ku_autoPhos': 0.1, + ... 'k_hydro': 0.5, 'ku_waste': 1.0, + ... 'kb_phosRP': 1.0, 'ku_phosRP': 0.1, + ... 'k_phosph': 0.5, 'ku_activeRP': 1.0, + ... 'ku_dephos': 0.01 + ... } + ... ) + ... mixture.compile_crn() """ @@ -31,7 +119,7 @@ def __init__( self, name='two_component_membrane_signaling', mechanism_type='membrane_sensor', - **keywords, + **kwargs, ): Mechanism.__init__(self, name, mechanism_type) @@ -45,8 +133,70 @@ def update_species( energy, waste, complex_dict=None, - **keywords, + **kwargs, ): + """Generate species for two-component membrane signaling pathway. + + Creates all species involved in the signaling cascade, including the + membrane sensor protein, response protein, signal substrate, ATP/ADP + energy species, and all intermediate complexes formed during signal + transduction and phosphotransfer. + + Parameters + ---------- + membrane_sensor_protein : Species + The membrane sensor protein that detects the signal. Must have + an ATP attribute specifying the number of ATP molecules required + for autophosphorylation. + response_protein : Species + The response protein that receives the phosphate group and + becomes activated. + assigned_substrate : Species + The phosphate substrate (Pi) that is transferred during the + signaling cascade. + signal_substrate : Species + The external signal molecule that activates the membrane sensor + protein. + product : Species + The phosphorylated response protein product (RP*). + energy : Species + ATP species used for autophosphorylation. + waste : Species + ADP species produced after ATP hydrolysis. + complex_dict : dict, optional + Pre-defined dictionary of complex species with keys + 'Activated_MSP', 'ATP:Activated_MSP', 'ADP:Activated_MSP:Sub', + 'Activated_MSP:Sub', 'Activated_MSP:Sub:RP', and + 'Activated_MSP:RP:Sub'. If None, complexes are automatically + created. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + List containing individual species and complex array: + [membrane_sensor_protein, response_protein, + assigned_substrate, signal_substrate, energy, waste, + complex_array] where complex_array is a list of all Complex + species generated. + + Notes + ----- + The method creates six different complex species representing the + intermediate states of the signaling cascade: + + 1. Activated_MSP : signal_substrate:membrane_sensor_protein + 2. ATP:Activated_MSP : nATP:Activated_MSP + 3. ADP:Activated_MSP:Sub : Activated_MSP:nADP:Pi + 4. Activated_MSP:Sub : Activated_MSP:Pi (phosphorylated sensor) + 5. Activated_MSP:Sub:RP : (Activated_MSP:Pi):response_protein + 6. Activated_MSP:RP:Sub : Activated_MSP:(response_protein:Pi) + + The number of ATP/ADP molecules (nATP) is determined by the + membrane_sensor_protein.ATP attribute. + + """ nATP = membrane_sensor_protein.ATP if complex_dict is None: @@ -116,12 +266,86 @@ def update_reactions( complex_dict=None, component=None, part_id=None, - **keywords, + **kwargs, ): - """Update reactions for membrane signaling pathway. + """Generate reactions for two-component membrane signaling pathway. + + Creates all eight reactions comprising the complete signaling + cascade from signal detection through response protein activation + and dephosphorylation. Reactions follow Michaelis-Menten kinetics + with reversible binding steps and irreversible catalytic steps. + + Parameters + ---------- + membrane_sensor_protein : Species + The membrane sensor protein that detects the signal. Must have + an ATP attribute specifying the number of ATP molecules required + for autophosphorylation. + response_protein : Species + The response protein that receives the phosphate group and + becomes activated. + assigned_substrate : Species + The phosphate substrate (Pi) that is transferred during the + signaling cascade. + signal_substrate : Species + The external signal molecule that activates the membrane sensor + protein. + product : Species + The phosphorylated response protein product (RP*). + energy : Species + ATP species used for autophosphorylation. + waste : Species + ADP species produced after ATP hydrolysis. + complex_dict : dict, optional + Pre-defined dictionary of complex species. If None, complexes + are automatically created using the same logic as in + update_species. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup in the component's parameter + database. Required for parameter lookup. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of eight reactions representing the complete signaling + cascade: + + 1. Signal binding (reversible) + 2. ATP binding (reversible) + 3. ATP hydrolysis (irreversible) + 4. ADP release (irreversible) + 5. Response protein binding (reversible) + 6. Phosphotransfer (irreversible) + 7. Activated response protein release (irreversible) + 8. Response protein dephosphorylation (irreversible) + + Raises + ------ + AttributeError + If component or part_id is None (required for parameter lookup). + + Notes + ----- + The reaction scheme follows this pathway: + + 1. SP + SigSub <--> SP:SigSub (rates: 'kb_sigMS', 'ku_sigMS') + 2. SP:SigSub + nATP <--> SP:SigSub:nATP + (rates: 'kb_autoPhos', 'ku_autoPhos') + 3. SP:SigSub:nATP --> SP:SigSub:Pi:nADP (rate: 'k_hydro') + 4. SP:SigSub:Pi:nADP --> SP:SigSub:Pi + nADP (rate: 'ku_waste') + 5. SP:SigSub:Pi + RP <--> SP:SigSub:Pi:RP + (rates: 'kb_phosRP', 'ku_phosRP') + 6. SP:SigSub:Pi:RP --> SP:SigSub:RP:Pi (rate: 'k_phosph') + 7. SP:SigSub:RP:Pi --> SP:SigSub + RP:Pi (rate: 'ku_activeRP') + 8. RP:Pi --> RP + Pi (rate: 'ku_dephos') - This always requires the inputs component and part_id to find - the relevant parameters. + This method requires both component and part_id parameters to + retrieve rate constants from the component's parameter database. """ # Get Parameters diff --git a/biocrnpyler/mechanisms/transport.py b/biocrnpyler/mechanisms/transport.py index bfb6affe..5a2206bc 100644 --- a/biocrnpyler/mechanisms/transport.py +++ b/biocrnpyler/mechanisms/transport.py @@ -8,18 +8,105 @@ class Simple_Diffusion(Mechanism): - """Diffusion of a substrate through a membrane channel. + """Passive diffusion mechanism for substrate transport across membranes. + + A 'diffusion' mechanism that models simple passive diffusion of + substrates through a membrane without requiring membrane proteins or + energy. The transport is bidirectional and follows Fick's law of + diffusion with equal forward and reverse rate constants. + + The reaction follows the schema: + + substrate <--> product + + where substrate and product represent the same species on opposite sides + of the membrane. + + Parameters + ---------- + name : str, default='simple_diffusion' + Name identifier for this mechanism instance. + mechanism_type : str, default='diffusion' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('diffusion'). + + See Also + -------- + Simple_Transport : Passive transport through membrane channels. + Facilitated_Transport_MM : Facilitated diffusion with carriers. + Mechanism : Base class for all mechanisms. + + Notes + ----- + Simple diffusion models the movement of small, lipophilic molecules + across lipid bilayers without the assistance of membrane proteins. This + process is driven purely by concentration gradients and does not require + cellular energy. + + Common examples include: + + - Diffusion of gases (O2, CO2) across cell membranes + - Transport of small nonpolar molecules + - Movement of lipid-soluble substances + + The mechanism generates a single reversible mass-action reaction with + equal forward and reverse rate constants, reflecting the thermodynamic + equilibrium of passive diffusion. + + Required parameters for this mechanism: + + - 'k_diff' : Diffusion rate constant (same for both directions) + + Examples + -------- + Model oxygen diffusion across a membrane: + + >>> O2 = bcp.DiffusibleMolecule('O2') + >>> mechanism = bcp.Simple_Diffusion() + >>> mixture = bcp.Mixture( + ... components=[O2], + ... mechanisms={'diffusion': mechanism}, + ... parameters={'k_diff': 0.1} + ... ) + >>> mixture.compile_crn() - Does not require energy and follows diffusion rules. - Reaction schema: substrate <-> product """ def __init__( - self, name='simple_diffusion', mechanism_type='diffusion', **keywords + self, name='simple_diffusion', mechanism_type='diffusion', **kwargs ): Mechanism.__init__(self, name, mechanism_type) - def update_species(self, substrate, product, **keywords): + def update_species(self, substrate, product, **kwargs): + """Generate species for simple diffusion. + + Returns the substrate and product species involved in the diffusion + reaction. + + Parameters + ---------- + substrate : Species + The substrate species on one side of the membrane (typically + the intracellular side). + product : Species + The product species on the other side of the membrane (typically + the extracellular side). Usually the same molecular species as + substrate but in a different compartment. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [substrate, product]. + + """ return [substrate, product] def update_reactions( @@ -29,8 +116,51 @@ def update_reactions( component=None, part_id=None, k_diff=None, - **keywords, + **kwargs, ): + """Generate reactions for simple diffusion. + + Creates a single reversible mass-action reaction representing + passive diffusion across a membrane with equal forward and reverse + rate constants. + + Parameters + ---------- + substrate : Species + The substrate species on one side of the membrane. + product : Species + The product species on the other side of the membrane. + component : Component, optional + Component containing parameter values. Required if k_diff is not + provided directly. + part_id : str, optional + Identifier for parameter lookup. If None and component is + provided, defaults to component.name. + k_diff : Parameter or float, optional + Diffusion rate constant. If None, retrieved from component + parameters. Used as both forward and reverse rate constant. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single reversible mass-action reaction for + diffusion. + + Raises + ------ + ValueError + If component is None and k_diff is not provided. + + Notes + ----- + The reaction has equal forward and reverse rate constants, reflecting + the thermodynamic equilibrium of passive diffusion: + + substrate <--> product (rates: 'k_diff', 'k_diff') + + """ # Get Parameters if part_id is None and component is not None: part_id = component.name @@ -56,24 +186,133 @@ def update_reactions( class Membrane_Protein_Integration(Mechanism): - """Integrate into the membrane protein in the membrane. + """Membrane protein integration mechanism for protein insertion. + + A 'membrane_insertion' mechanism that models the integration of newly + synthesized proteins into cellular membranes. Supports both monomeric + and oligomeric membrane proteins, handling oligomerization before + membrane insertion when required. + + The reaction schema depends on protein oligomeric state: + + For monomers (size = 1): + monomer --> integral membrane protein + + For oligomers (size > 1): + monomer * size <--> oligomer --> integral membrane protein + + Parameters + ---------- + name : str, default='membrane_protein_integration' + Name identifier for this mechanism instance. + mechanism_type : str, default='membrane_insertion' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('membrane_insertion'). + + See Also + -------- + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models the process by which proteins become embedded in + cellular membranes. For oligomeric proteins, multiple monomers must + first associate into a complex before integration can occur. The + integration step uses a `ProportionalHillNegative` propensity function to + model saturation kinetics and product inhibition. + + The mechanism requires the integral membrane protein to have a size + attribute (integral_membrane_protein.size) that specifies the number of + monomers in the functional unit. + + Common examples include: + + - Integration of ion channels (often oligomeric) + - Insertion of receptor proteins (can be monomeric or oligomeric) + - Assembly and insertion of transporter complexes + + Required parameters for this mechanism: + + - 'kb_oligomer' : Forward oligomerization rate constant (for size > 1) + - 'ku_oligomer' : Reverse oligomerization rate constant (for size > 1) + - 'kex' : Maximum integration rate constant + - 'kcat' : Michaelis constant for integration + + Examples + -------- + Model integration of a tetrameric channel: + + >>> channel = bcp.IntegralMembraneProtein( + ... membrane_protein='Aquaporin', + ... product='Aquaporin_channel', + ... size=2, + ... direction='Passive' + ... ) + >>> mechanism = bcp.Membrane_Protein_Integration() + >>> mixture = bcp.Mixture( + ... components=[channel], + ... mechanisms={'membrane_insertion': mechanism}, + ... parameters={ + ... 'kb_oligomer': 1.0, 'ku_oligomer': 0.1, + ... 'kex': 0.5, 'kcat': 10.0 + ... } + ... ) + >>> mixture.compile_crn() - Reaction schema for monomers: monomer -> intergral membrane protein - Reaction schema for oligomer: monomer*[size] -> oligomer - -> intergral membrane protein """ def __init__( self, name='membrane_protein_integration', mechanism_type='membrane_insertion', - **keywords, + **kwargs, ): Mechanism.__init__(self, name, mechanism_type) def update_species( - self, integral_membrane_protein, product, complex=None, **keywords + self, integral_membrane_protein, product, complex=None, **kwargs ): + """Generate species for membrane protein integration. + + Creates species for monomers, oligomeric complexes (if needed), and + the integrated membrane protein product. + + Parameters + ---------- + integral_membrane_protein : Species + The membrane protein monomer that will be integrated. Must have + a size attribute specifying oligomeric state. + product : Species + The integrated membrane protein product after insertion. + complex : Species, optional + Pre-specified oligomeric complex. If None and size > 1, + automatically creates a Complex of size monomers. Ignored for + monomeric proteins (size = 1). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + List containing [integral_membrane_protein, product, complex] + where complex is None for monomers or a Complex species for + oligomers. + + Notes + ----- + For monomeric proteins (size = 1), no oligomeric complex is formed + and the complex element in the return list is None. + + For oligomeric proteins (size > 1), a complex containing 'size' + copies of the monomer is created or used if provided. + + """ if complex is None: size = integral_membrane_protein.size if size > 1: @@ -95,12 +334,62 @@ def update_reactions( complex=None, component=None, part_id=None, - **keywords, + **kwargs, ): - """Update reactions for membrane integration. - - This always requires the inputs component and part_id to find - the relevant parameters. + """Generate reactions for membrane protein integration. + + Creates reactions for oligomerization (if needed) and membrane + integration. For oligomeric proteins, generates both oligomerization + and integration reactions. For monomers, generates only the + integration reaction. + + Parameters + ---------- + integral_membrane_protein : Species + The membrane protein monomer. Must have a size attribute. + product : Species + The integrated membrane protein product. + complex : Species, optional + Pre-specified oligomeric complex. If None and size > 1, + automatically created. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup in the component's parameter + database. Required for parameter lookup. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + For oligomers (size > 1): List of two reactions + [oligomerization, integration]. + For monomers (size = 1): List of one reaction [integration]. + + Raises + ------ + AttributeError + If component or part_id is None (required for parameter lookup). + + Notes + ----- + The reaction scheme depends on oligomeric state: + + For oligomers (size > 1): + + 1. size * monomer <--> oligomer (rates: 'kb_oligomer', + 'ku_oligomer') + 2. oligomer --> product (ProportionalHillNegative with 'kex', + 'kcat') + + For monomers (size = 1): + + 1. monomer --> product (ProportionalHillNegative with 'kex', 'kcat') + + The integration reaction uses `ProportionalHillNegative` kinetics with + Hill coefficient n=4 to model saturation and product inhibition. """ # Get Parameters @@ -164,11 +453,93 @@ def update_reactions( class Simple_Transport(Mechanism): - """Transport of a substrate through a membrane channel. - - Does not require energy and has unidirectional transport, - following diffusion rules. Reaction schema: membrane_channel + - substrate <-> membrane_channel + product + """Passive transport mechanism through membrane channel proteins. + + A 'transport' mechanism that models passive, bidirectional transport of + substrates through membrane channel proteins. Unlike simple diffusion, + this mechanism requires a membrane channel protein but does not consume + energy. The channel acts catalytically, binding substrate and product + but not being consumed. + + The reaction follows the schema: + + membrane_channel + substrate <--> membrane_channel + product + + Parameters + ---------- + name : str, default='simple_membrane_protein_transport' + Name identifier for this mechanism instance. + mechanism_type : str, default='transport' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transport'). + + See Also + -------- + Simple_Diffusion : Passive diffusion without proteins. + Facilitated_Transport_MM : Transport with MM kinetics. + Primary_Active_Transport_MM : Energy-dependent active transport. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models passive transport through channel proteins such as + ion channels, aquaporins, and other pore-forming proteins. The channel + facilitates movement down concentration gradients without conformational + changes or energy expenditure. + + The mechanism requires the membrane channel to have the 'Passive' + attribute, distinguishing it from active transporters and carriers that + require different mechanisms. + + Common examples include: + + - Ion channels (K+, Na+, Ca2+ channels) + - Aquaporins for water transport + - Gap junctions between cells + - Porins in bacterial outer membranes + + The transport is bidirectional with equal forward and reverse rate + constants, reflecting passive equilibration across the membrane. + + Required parameters for this mechanism: + + - 'k_trnsp' : Transport rate constant (same for both directions) + + Examples + -------- + Model potassium transport through an ion channel: + + >>> protein = bcp.IntegralMembraneProtein( + ... membrane_protein='Knck1', + ... product='K_channel', + ... direction='Passive', + ... compartment='cytoplasm', + ... membrane_compartment='membrane', + ... attributes=['Passive'] + ... ) + >>> channel = bcp.MembraneChannel( + ... integral_membrane_protein=protein.membrane_protein, + ... substrate='K', + ... direction='Passive', + ... internal_compartment='cytoplasm', + ... external_compartment='external' + ... ) + >>> mixture = bcp.Mixture( + ... components=[protein, channel], + ... mechanisms={ + ... 'membrane_insertion': bcp.Membrane_Protein_Integration(), + ... 'transport': bcp.Simple_Transport(), + ... }, + ... parameters={'k_trnsp': 1.0}, + ... parameter_file='mechanisms/transport_parameters.tsv', + ... ) + >>> mixture.compile_crn() """ @@ -176,18 +547,49 @@ def __init__( self, name='simple_membrane_protein_transport', mechanism_type='transport', - **keywords, + **kwargs, ): Mechanism.__init__(self, name, mechanism_type) - def update_species( - self, membrane_channel, substrate, product, **keywords - ): + def update_species(self, membrane_channel, substrate, product, **kwargs): + """Generate species for simple transport. + + Returns the membrane channel, substrate, and product species + involved in the transport reaction. Validates that the channel has + the 'Passive' attribute. + + Parameters + ---------- + membrane_channel : Species + The membrane channel protein through which transport occurs. + Must have 'Passive' as its first attribute. + substrate : Species + The substrate species being transported (typically intracellular + side). + product : Species + The product species after transport (typically extracellular + side). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [membrane_channel, substrate, product]. + + Raises + ------ + ValueError + If membrane_channel does not have 'Passive' as its first + attribute, indicating it should use Facilitated_Transport_MM + instead. + + """ if membrane_channel.attributes[0] != 'Passive': raise ValueError( "Protein is not classified as a channel with passive " "transport of small molecules. Use mechanism " - "Facilitated_Passive_Transport instead" + "Facilitated_Transport_MM instead" ) return [membrane_channel, substrate, product] @@ -200,8 +602,57 @@ def update_reactions( component=None, part_id=None, k_trnsp=None, - **keywords, + **kwargs, ): + """Generate reactions for simple membrane protein transport. + + Creates a single reversible mass-action reaction representing + passive transport through a membrane channel with equal forward and + reverse rate constants. The channel acts catalytically and is not + consumed. + + Parameters + ---------- + membrane_channel : Species + The membrane channel protein facilitating transport. + substrate : Species + The substrate species being transported. + product : Species + The product species after transport. + component : Component, optional + Component containing parameter values. Required if k_trnsp is + not provided directly. + part_id : str, optional + Identifier for parameter lookup. If None and component is + provided, defaults to component.name. + k_trnsp : Parameter or float, optional + Transport rate constant. If None, retrieved from component + parameters. Used as both forward and reverse rate constant. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single reversible mass-action reaction for + transport. + + Raises + ------ + ValueError + If component is None and k_trnsp is not provided. + + Notes + ----- + The reaction has equal forward and reverse rate constants: + + membrane_channel + substrate <--> membrane_channel + product + (rates: 'k_trnsp', 'k_trnsp') + + The membrane channel appears on both sides of the reaction, + indicating it acts catalytically and is recycled. + + """ # Get Parameters if part_id is None and component is not None: part_id = component.name @@ -219,22 +670,107 @@ def update_reactions( # Simple membrane protein transport # Sub (Internal) <--> Product (External) - SimpleTransport_rxn = Reaction.from_massaction( + Simple_Transport_rxn = Reaction.from_massaction( inputs=[substrate, membrane_channel], outputs=[product, membrane_channel], k_forward=k_trnsp, k_reverse=k_trnsp, ) - return [SimpleTransport_rxn] + return [Simple_Transport_rxn] class Facilitated_Transport_MM(Mechanism): - """Michaelis-Menten transport of a substrate through a membrane carrier. - - Mechanism follows Michaelis-Menten Type Reactions with products - that can bind to membrane carriers. Mechanism for the schema: - - Sub+MC <--> Sub:MC --> Prod:MC --> Prod + MC + """Facilitated diffusion mechanism with Michaelis-Menten kinetics. + + A 'transport' mechanism that models facilitated diffusion of substrates + through membrane carrier proteins. Unlike simple channels, carriers + undergo conformational changes to transport substrates across membranes. + The mechanism follows Michaelis-Menten kinetics with explicit substrate + and product binding steps. + + The reaction follows the schema: + + Sub + MC <--> Sub:MC --> Prod:MC --> Prod + MC + + where MC is the membrane carrier protein. + + Parameters + ---------- + name : str, default='facilitated_membrane_protein_transport' + Name identifier for this mechanism instance. + mechanism_type : str, default='transport' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transport'). + + See Also + -------- + Simple_Transport : Passive transport through channels. + Primary_Active_Transport_MM : Energy-dependent active transport. + MichaelisMenten : Enzyme mechanism with similar kinetics. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models facilitated diffusion by carrier proteins that + alternate between substrate-bound and product-bound conformations. The + carrier binds substrate on one side of the membrane, undergoes a + conformational change to transport it across, releases it as product, + and returns to the original conformation. + + Key characteristics: + + - Does not require ATP or other energy sources + - Transport is driven by concentration gradients + - Carrier proteins alternate between conformational states + - Follows Michaelis-Menten saturation kinetics + + Common examples include: + + - GLUT transporters for glucose + - Amino acid carriers + - Nucleoside transporters + - Urea transporters + + The mechanism uses a GeneralPropensity with a Heaviside function for + the initial binding step to enforce directionality based on + concentration gradients. + + Required parameters for this mechanism: + + - 'kb_subMC' : Forward binding rate for substrate to membrane carrier + - 'ku_subMC' : Unbinding rate for substrate from carrier + - 'k_trnspMC' : Conformational change rate (transport step) + - 'ku_prodMC' : Unbinding rate for product from carrier + + Examples + -------- + Model glucose transport through a GLUT transporter: + + >>> glc_in = bcp.Species('glucose', compartment='cytoplasm') + >>> glc_out = bcp.Species('glucose', compartment='external') + >>> carrier = bcp.MembraneChannel( + ... integral_membrane_protein='GlucoseTransporter', + ... substrate=glc_out, + ... external_compartment='external', + ... internal_compartment='cytoplasm', + ... direction='Importer' + ... ) + >>> mechanism = bcp.Facilitated_Transport_MM() + >>> mixture = bcp.Mixture( + ... components=[carrier], + ... mechanisms={'transport': mechanism}, + ... parameters={ + ... 'kb_subMC': 1.0, 'ku_subMC': 0.5, + ... 'k_trnspMC': 0.8, 'ku_prodMC': 0.5 + ... } + ... ) + >>> mixture.compile_crn() """ @@ -242,7 +778,7 @@ def __init__( self, name='facilitated_membrane_protein_transport', mechanism_type='transport', - **keywords, + **kwargs, ): Mechanism.__init__(self, name, mechanism_type) @@ -252,8 +788,46 @@ def update_species( substrate, product, complex_dict=None, - **keywords, + **kwargs, ): + """Generate species for facilitated transport. + + Creates species for the membrane carrier, substrate, product, and + the two intermediate complexes formed during the transport cycle. + + Parameters + ---------- + membrane_carrier : Species + The membrane carrier protein that facilitates transport. + substrate : Species + The substrate species being transported (typically intracellular + side). + product : Species + The product species after transport (typically extracellular + side). Usually the same molecular species as substrate but in a + different compartment. + complex_dict : dict, optional + Pre-defined dictionary of complex species with keys 'sub:MC' and + 'prod:MC'. If None, complexes are automatically created. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + List containing [membrane_carrier, substrate, product, + complex_array] where complex_array is a list of two Complex + species: [substrate:carrier, product:carrier]. + + Notes + ----- + The method creates two complex species representing intermediates in + the transport cycle: + + 1. sub:MC : substrate:membrane_carrier complex + 2. prod:MC : product:membrane_carrier complex + + """ if complex_dict is None: # Create empty dictionary for complexes complex_dict = {} @@ -281,12 +855,60 @@ def update_reactions( complex_dict=None, component=None, part_id=None, - **keywords, + **kwargs, ): - """Update reactions for facilitated transport mechanism. - - This always requires the inputs component and part_id to find - the relevant parameters. + """Generate reactions for facilitated transport. + + Creates four reactions representing the complete transport cycle: + substrate binding, substrate unbinding, conformational change + (transport), and product release. + + Parameters + ---------- + membrane_carrier : Species + The membrane carrier protein facilitating transport. + substrate : Species + The substrate species being transported. + product : Species + The product species after transport. + complex_dict : dict, optional + Pre-defined dictionary of complex species. If None, complexes + are automatically created using the same logic as in + update_species. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup in the component's parameter + database. Required for parameter lookup. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of four reactions: [substrate_binding, substrate_unbinding, + transport_step, product_release]. + + Raises + ------ + AttributeError + If component or part_id is None (required for parameter lookup). + + Notes + ----- + The reaction scheme follows this pathway: + + 1. MC + Sub <--> MC:Sub (GeneralPropensity with Heaviside function + using 'kb_subMC') + 2. MC:Sub --> MC + Sub (irreversible, rate: 'ku_subMC') + 3. MC:Sub --> MC:Prod (conformational change, rate: 'k_trnspMC') + 4. MC:Prod --> MC + Prod (irreversible, rate: 'ku_prodMC') + + The initial binding step uses a GeneralPropensity with a Heaviside + function to enforce concentration gradient-driven directionality. + The Heaviside function ensures transport only occurs when substrate + concentration exceeds product concentration. """ # Get Parameters @@ -358,13 +980,103 @@ def update_reactions( class Primary_Active_Transport_MM(Mechanism): - """Transport of a substrate through a membrane carrier. - - Mechanism follows Michaelis-Menten Type Reactions with products - that can bind to membrane carriers. Mechanism for the schema: - - Sub+MP <--> Sub:MP + E --> Sub:MP:E --> MP:Prod:E - --> Prod + MP:W --> Prod + MP+ W + """Primary active transport mechanism with ATP-dependent pumping. + + A 'transport' mechanism that models primary active transport where + substrates are moved against their concentration gradients using energy + from ATP hydrolysis. The mechanism follows Michaelis-Menten kinetics + with explicit binding, ATP hydrolysis, conformational change, and + product release steps. + + The reaction follows the schema: + + Sub + MP <--> Sub:MP + E --> Sub:MP:E --> MP:Prod:E + --> Prod + MP:W --> Prod + MP + W + + where MP is the membrane pump, E is ATP (energy), and W is ADP (waste). + + Parameters + ---------- + name : str, default='active_membrane_protein_transport' + Name identifier for this mechanism instance. + mechanism_type : str, default='transport' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transport'). + + See Also + -------- + Facilitated_Transport_MM : Passive facilitated diffusion. + Simple_Transport : Passive channel transport. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models primary active transporters such as P-type ATPases + (e.g., Na+/K+-ATPase, Ca2+-ATPase), ABC transporters, and other pumps + that directly couple ATP hydrolysis to substrate transport. The pump + undergoes conformational changes driven by ATP binding and hydrolysis to + move substrates against concentration gradients. + + Key characteristics: + + - Requires ATP or other energy source + - Can transport substrates against concentration gradients + - Undergoes ATP-dependent conformational changes + - Follows Michaelis-Menten saturation kinetics + + Common examples include: + + - Na+/K+-ATPase (maintains ion gradients in animal cells) + - Ca2+-ATPase (SERCA pump in muscle cells) + - H+-ATPases (proton pumps in various organisms) + - ABC transporters (drug efflux pumps) + + The mechanism requires the membrane pump to have an ATP attribute + (membrane_pump.ATP) that specifies the number of ATP molecules required + per transport cycle. + + The binding steps use GeneralPropensity with Heaviside functions to + ensure proper directionality based on species concentrations. + + Required parameters for this mechanism: + + - 'kb_subMP' : Forward binding rate for substrate to membrane pump + - 'ku_subMP' : Unbinding rate for substrate from pump + - 'kb_subMPnATP' : Forward binding rate for ATP to substrate:pump + complex + - 'ku_subMPnATP' : Unbinding rate for ATP from substrate:pump complex + - 'k_trnspMP' : Conformational change rate (transport step) + - 'ku_prodMP' : Unbinding rate for product from pump + - 'ku_MP' : Unbinding rate for ADP from pump + + Examples + -------- + Model active sodium transport by Na+/K+-ATPase: + + >>> pump = bcp.MembranePump( + ... membrane_pump='NaK_ATPase', + ... substrate='Na', + ... direction='Exporter', + ... ATP=1 + ... ) + >>> mechanism = bcp.Primary_Active_Transport_MM() + >>> mixture = bcp.Mixture( + ... components=[pump], + ... mechanisms={'transport': mechanism}, + ... parameters={ + ... 'kb_subMP': 1.0, 'ku_subMP': 0.1, + ... 'kb_subMPnATP': 1.0, 'ku_subMPnATP': 0.1, + ... 'k_trnspMP': 0.5, 'ku_prodMP': 1.0, + ... 'ku_MP': 1.0 + ... } + ... ) + >>> mixture.compile_crn() """ @@ -372,7 +1084,7 @@ def __init__( self, name='active_membrane_protein_transport', mechanism_type='transport', - **keywords, + **kwargs, ): Mechanism.__init__(self, name, mechanism_type) @@ -384,8 +1096,59 @@ def update_species( energy, waste, complex_dict=None, - **keywords, + **kwargs, ): + """Generate species for primary active transport. + + Creates species for the membrane pump, substrate, product, ATP/ADP + energy species, and all intermediate complexes formed during the + ATP-driven transport cycle. + + Parameters + ---------- + membrane_pump : Species + The membrane pump protein that transports substrates using ATP. + Must have an ATP attribute specifying the number of ATP + molecules required per transport cycle. + substrate : Species + The substrate species being transported (typically intracellular + side). + product : Species + The product species after transport (typically extracellular + side). Usually the same molecular species as substrate but in a + different compartment. + energy : Species + ATP species used to drive active transport. + waste : Species + ADP species produced after ATP hydrolysis. + complex_dict : dict, optional + Pre-defined dictionary of complex species with keys 'Pump:Sub', + 'Pump:Sub:ATP', 'Pump:Prod:ATP', and 'Pump:ADP'. If None, + complexes are automatically created. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list + List containing [membrane_pump, substrate, product, energy, + waste, complex_array] where complex_array is a list of four + Complex species generated. + + Notes + ----- + The method creates four complex species representing intermediates + in the active transport cycle: + + 1. Pump:Sub : membrane_pump:substrate complex + 2. Pump:Sub:ATP : membrane_pump:substrate:nATP complex + 3. Pump:Prod:ATP : membrane_pump:product:nATP complex + 4. Pump:ADP : membrane_pump:nADP complex + + The number of ATP/ADP molecules (nATP) is determined by the + membrane_pump.ATP attribute. + + """ nATP = membrane_pump.ATP if complex_dict is None: @@ -434,12 +1197,73 @@ def update_reactions( complex_dict=None, component=None, part_id=None, - **keywords, + **kwargs, ): - """Update reactions for primary active transport mechanism. - - This always requires the inputs component and part_id to find - the relevant parameters. + """Generate reactions for primary active transport. + + Creates seven reactions representing the complete ATP-driven + transport cycle: substrate binding/unbinding, ATP + binding/unbinding, conformational change (transport), product + release, and ADP release. + + Parameters + ---------- + membrane_pump : Species + The membrane pump protein. Must have an ATP attribute. + substrate : Species + The substrate species being transported. + product : Species + The product species after transport. + energy : Species + ATP species used for active transport. + waste : Species + ADP species produced after ATP hydrolysis. + complex_dict : dict, optional + Pre-defined dictionary of complex species. If None, complexes + are automatically created using the same logic as in + update_species. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup in the component's parameter + database. Required for parameter lookup. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of seven reactions: [substrate_binding, + substrate_unbinding, ATP_binding, ATP_unbinding, transport_step, + product_release, ADP_release]. + + Raises + ------ + AttributeError + If `component` or `part_id` is None (required for parameter + lookup). + + Notes + ----- + The reaction scheme follows this pathway: + + 1. MP + Sub <--> MP:Sub (`GeneralPropensity` with Heaviside using + 'kb_subMP', reverse rate: 'ku_subMP') + 2. MP:Sub + nATP <--> MP:Sub:nATP (`GeneralPropensity` with Heaviside + using 'kb_subMPnATP', reverse rate: 'ku_subMPnATP') + 3. MP:Sub:nATP --> MP:Prod:nATP (conformational change, rate: + 'k_trnspMP') + 4. MP:Prod:nATP --> MP:nADP + Prod (product release, rate: + 'ku_prodMP') + 5. MP:nADP --> MP + nADP (ADP release, rate: 'ku_MP') + + The binding steps use `GeneralPropensity` with Heaviside functions to + enforce proper directionality. The Heaviside functions ensure that + reactions only proceed when the required species are present. + + The number of ATP/ADP molecules (nATP) is determined by + membrane_pump.ATP attribute. """ # Get Parameters diff --git a/biocrnpyler/mechanisms/txtl.py b/biocrnpyler/mechanisms/txtl.py index a0711526..c40b38f9 100644 --- a/biocrnpyler/mechanisms/txtl.py +++ b/biocrnpyler/mechanisms/txtl.py @@ -12,23 +12,113 @@ class OneStepGeneExpression(Mechanism): - """Gene expression without transcription or translation. + """Single-step gene expression mechanism without explicit TX-TL steps. + + A 'transcription' mechanism that models gene expression as a single + direct reaction from DNA to protein, without explicitly modeling + transcription and translation as separate steps. This simplified + mechanism is useful for models where the intermediate mRNA dynamics are + not important. + + The reaction follows the schema: G --> G + P + + where G is the gene (DNA) and P is the protein product. + + Parameters + ---------- + name : str, default='gene_expression' + Name identifier for this mechanism instance. + mechanism_type : str, default='transcription' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + SimpleTranscription : Explicit transcription mechanism. + SimpleTranslation : Explicit translation mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is appropriate for modeling scenarios where: + + - mRNA dynamics are much faster than protein dynamics + - The model focuses on protein-level behavior + - Computational efficiency is prioritized over mechanistic detail + + The single-step abstraction combines transcription and translation into + a single effective rate constant. This is often used in coarse-grained + models of gene regulatory networks. + + Common applications include: + + - Simplified gene regulatory network models + - Steady-state or quasi-steady-state analyses + - Systems where mRNA lifetime is negligible + - High-level circuit design and prototyping + + Required parameters for this mechanism: + + - 'kexpress' : Combined expression rate constant + + Examples + -------- + Model simple gene expression in a minimal system: + + >>> gene = bcp.DNAassembly( + ... name='gfp', promoter='pconst', protein='GFP', + ... ) + >>> mechanism = bcp.OneStepGeneExpression() + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': mechanism, + ... }, + ... parameters={'kexpress': 0.1} + ... ) + >>> mixture.compile_crn() + """ def __init__( self, name='gene_expression', mechanism_type='transcription' ): - """Initializes a OneStepGeneExpression instance. + Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - :param name: name of the Mechanism, default: gene_expression - :param mechanism_type: type of the Mechanism, default: transcription + def update_species(self, dna, transcript=None, protein=None, **kwargs): + """Generate species for one-step gene expression. + + Returns the DNA and protein species involved in the expression + reaction. + + Parameters + ---------- + dna : Species + The DNA species (gene) that expresses the protein. + transcript : Species, optional + Transcript species (unused in this mechanism, accepted for API + consistency). + protein : Species + The protein species produced by expression. If None, no species + are returned. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [dna, protein] if protein is not None, otherwise + [dna]. """ - Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - - def update_species(self, dna, protein, transcript=None, **keywords): species = [dna] if protein is not None: species += [protein] @@ -43,8 +133,55 @@ def update_reactions( protein=None, transcript=None, part_id=None, - **keywords, + **kwargs, ): + """Generate reactions for one-step gene expression. + + Creates a single mass-action reaction representing direct gene + expression from DNA to protein without intermediate transcript. + + Parameters + ---------- + dna : Species + The DNA species (gene) that expresses the protein. + component : Component, optional + Component containing parameter values. Required if kexpress is + not provided directly. + kexpress : Parameter or float, optional + Expression rate constant. If None, retrieved from component + parameters. + protein : Species, optional + The protein species produced. If None, no reactions are + generated. + transcript : Species, optional + Transcript species (unused in this mechanism, accepted for API + consistency). + part_id : str, optional + Identifier for parameter lookup in the component's parameter + database. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single mass-action reaction for gene + expression if protein is not None, otherwise empty list. + + Raises + ------ + ValueError + If component is None and kexpress is not provided. + + Notes + ----- + The reaction has the form: + + DNA --> DNA + Protein (rate: 'kexpress') + + The DNA is catalytic and appears on both sides of the reaction. + + """ if kexpress is None and component is not None: kexpress = component.get_parameter( 'kexpress', part_id=part_id, mechanism=self @@ -63,23 +200,114 @@ def update_reactions( class SimpleTranscription(Mechanism): - """A Mechanism to model simple catalytic transcription. + """Simple catalytic transcription mechanism. + + A 'transcription' mechanism that models transcription as a single + catalytic reaction where DNA directly produces mRNA without explicitly + modeling RNA polymerase binding and unbinding. This simplified mechanism + is appropriate when polymerase dynamics are fast compared to other + processes. + + The reaction follows the schema: G --> G + T + + where G is the gene (DNA) and T is the transcript (mRNA). + + Parameters + ---------- + name : str, default='simple_transcription' + Name identifier for this mechanism instance. + mechanism_type : str, default='transcription' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + Transcription_MM : Explicit Michaelis-Menten transcription. + OneStepGeneExpression : Combined transcription-translation. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is appropriate for modeling scenarios where: + + - RNA polymerase dynamics are fast and at quasi-steady-state + - The focus is on transcript-level regulation + - Computational efficiency is prioritized + + The catalytic abstraction treats the DNA as a template that is not + consumed in the reaction. In mixtures without explicit transcription + (e.g., expression mixtures), this mechanism can combine with translation + rates to produce protein directly. + + Common applications include: + + - Basic transcription models + - Models where RNAP is not limiting + - Constitutive or weakly regulated promoters + + Required parameters for this mechanism: + + - 'ktx' : Transcription rate constant + - 'ktl' : Translation rate constant (when used in expression mixtures + without explicit transcripts) + + Examples + -------- + Model constitutive transcription: + + >>> gene = bcp.DNAassembly( + ... name='constitutive_GFP', + ... promoter='pconst', protein='GFP') + >>> tx_mechanism = bcp.SimpleTranscription() + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': tx_mechanism, + ... }, + ... parameters={'ktx': 0.05} + ... ) + >>> mixture.compile_crn() + """ def __init__( self, name='simple_transcription', mechanism_type='transcription' ): - """Initializes a SimpleTranscription instance. + Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - :param name: name of the Mechanism, default: simple_transcription - :param mechanism_type: type of the Mechanism, default: transcription + def update_species(self, dna, transcript=None, protein=None, **kwargs): + """Generate species for simple transcription. + + Returns the DNA and transcript species (and optionally protein) + involved in the transcription reaction. + + Parameters + ---------- + dna : Species + The DNA species (gene) that produces the transcript. + transcript : Species, optional + The mRNA transcript species produced. If None, only DNA is + returned. + protein : Species, optional + Protein species (included for API consistency with expression + mixtures). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing DNA and any non-None transcript/protein species. """ - Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - - def update_species(self, dna, transcript=None, protein=None, **keywords): species = [dna] if transcript is not None: species += [transcript] @@ -96,8 +324,58 @@ def update_reactions( part_id=None, transcript=None, protein=None, - **keywords, + **kwargs, ): + """Generate reactions for simple transcription. + + Creates a single mass-action reaction representing transcription from + DNA to mRNA. In expression mixtures without explicit transcription, + combines transcription and translation rates to produce protein + directly. + + Parameters + ---------- + dna : Species + The DNA species (gene) that produces the transcript. + component : Component, optional + Component containing parameter values. Required if ktx is not + provided directly. + ktx : Parameter or float, optional + Transcription rate constant. If None, retrieved from component + parameters. + part_id : str, optional + Identifier for parameter lookup in the component's parameter + database. + transcript : Species, optional + The mRNA transcript species produced. If None and protein is + not None, produces protein directly. + protein : Species, optional + Protein species for expression mixtures without explicit + transcription. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single mass-action reaction for transcription. + + Raises + ------ + ValueError + If component is None and ktx is not provided. + + Notes + ----- + The reaction has two possible forms: + + 1. With transcript: DNA --> DNA + mRNA (rate: 'ktx') + 2. Without transcript: DNA --> DNA + Protein (rate: 'ktx' * 'ktl') + + The second form is used in expression mixtures that skip explicit + transcript modeling. + + """ if ktx is None and component is not None: ktx = component.get_parameter( 'ktx', part_id=part_id, mechanism=self @@ -127,23 +405,111 @@ def update_reactions( class SimpleTranslation(Mechanism): - """A mechanism to model simple catalytic translation. + """Simple catalytic translation mechanism. + + A 'translation' mechanism that models translation as a single catalytic + reaction where mRNA directly produces protein without explicitly modeling + ribosome binding and unbinding. This simplified mechanism is appropriate + when ribosome dynamics are fast compared to other processes. + + The reaction follows the schema: T --> T + P + + where T is the transcript (mRNA) and P is the protein. + + Parameters + ---------- + name : str, default='simple_translation' + Name identifier for this mechanism instance. + mechanism_type : str, default='translation' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('translation'). + + See Also + -------- + Translation_MM : Explicit Michaelis-Menten translation. + SimpleTranscription : Simple transcription mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism is appropriate for modeling scenarios where: + + - Ribosome dynamics are fast and at quasi-steady-state + - The focus is on protein-level regulation + - Computational efficiency is prioritized + + The catalytic abstraction treats the mRNA as a template that is not + consumed in the reaction. The mRNA persists and can produce multiple + protein copies. + + Common applications include: + + - Basic translation models + - Models where ribosomes are not limiting + - Constitutive protein expression + + Required parameters for this mechanism: + + - 'ktl' : Translation rate constant + + Examples + -------- + Model simple translation: + + >>> gene = bcp.DNAassembly( + ... name='dna_assembly', + ... promoter='pconst', rbs='RBS_medium', protein='GFP') + >>> tl_mechanism = bcp.SimpleTranslation() + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': tl_mechanism + ... }, + ... parameter_file='mixtures/pure_parameters.tsv' + ... ) + >>> mixture.compile_crn() + """ def __init__( self, name='simple_translation', mechanism_type='translation' ): - """Initializes a SimpleTranslation instance. + Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - :param name: name of the Mechanism, default: simple_translation - :param mechanism_type: type of the Mechanism, default: translation + def update_species(self, transcript, protein=None, **kwargs): + """Generate species for simple translation. + + Returns the transcript and protein species involved in the + translation reaction. If protein is None, creates a default protein + species based on the transcript name. + + Parameters + ---------- + transcript : Species + The mRNA transcript species that produces protein. + protein : Species or list of Species, optional + The protein species produced. If None, a default protein with + the same name as the transcript is created. Can be a single + Species or a list. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [transcript] plus protein species (single or + list). """ - Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) - - def update_species(self, transcript, protein=None, **keywords): if protein is None: protein = Species(transcript.name, material_type='protein') outlst = [transcript] @@ -160,8 +526,53 @@ def update_reactions( ktl=None, part_id=None, protein=None, - **keywords, + **kwargs, ): + """Generate reactions for simple translation. + + Creates a single mass-action reaction representing translation from + mRNA to protein. + + Parameters + ---------- + transcript : Species + The mRNA transcript species that produces protein. + component : Component, optional + Component containing parameter values. Required if ktl is not + provided directly. + ktl : Parameter or float, optional + Translation rate constant. If None, retrieved from component + parameters. + part_id : str, optional + Identifier for parameter lookup in the component's parameter + database. + protein : Species, optional + The protein species produced. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing a single mass-action reaction for translation if + transcript is not None, otherwise empty list. + + Raises + ------ + ValueError + If component is None and ktl is not provided. + + Notes + ----- + The reaction has the form: + + mRNA --> mRNA + Protein (rate: 'ktl') + + The mRNA is catalytic and appears on both sides of the reaction. If + transcript is None (only occurs in mixtures without transcription), + no reactions are generated. + + """ if ktl is None and component is not None: ktl = component.get_parameter( 'ktl', part_id=part_id, mechanism=self @@ -186,14 +597,96 @@ def update_reactions( class PositiveHillTranscription(Mechanism): - """Model transcription as a proprotional positive Hill function. + """Transcription regulated by positive Hill function (activation). - G --> G + P + A 'transcription' mechanism that models transcriptional activation using + a proportional positive Hill function. The transcription rate increases + with regulator (activator) concentration according to Hill kinetics, + capturing cooperative binding and activation. + + The reaction follows the schema: + + G --> G + T + + with rate: + + rate = k * G * (R^n) / (K + R^n) + + where R is the regulator (activator), n is the Hill coefficient, and K + is the activation constant. Optionally includes a basal leak reaction at + rate 'kleak'. + + Parameters + ---------- + name : str, default='positivehill_transcription' + Name identifier for this mechanism instance. + mechanism_type : str, default='transcription' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + NegativeHillTranscription : Transcriptional repression with Hill + function. + SimpleTranscription : Simple transcription without regulation. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models transcriptional activation by regulatory proteins + such as transcription factors. The Hill function captures the sigmoidal + response typical of cooperative binding, where multiple regulator + molecules bind to the promoter to activate transcription. + + Key features: + + - Sigmoidal activation response + - Cooperative binding through Hill coefficient + - Optional basal transcription (leak) + - Saturation at high regulator concentrations + + Common applications include: + + - Activatable promoters + - Positive feedback loops + - Transcriptional cascades + - Genetic switches and toggles + + Required parameters for this mechanism: + + - 'k' : Maximum transcription rate constant + - 'K' : Activation constant (regulator concentration for half-maximal + activation) + - 'n' : Hill coefficient (cooperativity) + - 'kleak' : Basal transcription rate (optional, for leak reaction) + + Examples + -------- + Model transcriptional activation by an inducer: + + >>> LacI = bcp.Protein('AraC') + >>> plac = bcp.ActivatablePromoter('pBAD', LacI) + >>> gene = bcp.DNAassembly( + ... name='activiated_GFP', + ... promoter=plac, rbs='RBS_medium', protein='GFP') + >>> tx_mechanism = bcp.PositiveHillTranscription() + >>> mixture = bcp.Mixture( + ... components=[LacI, gene], + ... mechanisms={ + ... 'transcription': tx_mechanism, + ... 'translation': bcp.SimpleTranslation(), + ... }, + ... parameters={'k': 1.0, 'K': 10.0, 'n': 2, 'kleak': 0.01}, + ... parameter_file='mixtures/pure_parameters.tsv', + ... ) + >>> mixture.compile_crn() - rate = k*G*(R^n)/(K+R^n) - where R is a regulator (activator). - Optionally includes a leak reaction - G --> G + P @ rate kleak. """ def __init__( @@ -201,13 +694,6 @@ def __init__( name='positivehill_transcription', mechanism_type='transcription', ): - """Initializes a PositiveHillTranscription instance. - - :param name: name of the Mechanism, default: - positivehill_transcription - :param mechanism_type: type of the Mechanism, default: transcription - - """ Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) def update_species( @@ -217,8 +703,37 @@ def update_species( transcript=None, leak=False, protein=None, - **keywords, + **kwargs, ): + """Generate species for positive Hill transcription. + + Returns all species involved in the regulated transcription + reaction. + + Parameters + ---------- + dna : Species + The DNA species (promoter) being transcribed. + regulator : Species + The activator species that regulates transcription. + transcript : Species, optional + The mRNA transcript species produced. If None and protein is + provided, protein is produced directly. + leak : bool, default=False + If True, includes a leak reaction for basal transcription. + protein : Species, optional + Protein species for expression mixtures without explicit + transcription. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [dna, regulator] plus any non-None + transcript/protein species. + + """ species = [dna, regulator] if transcript is not None: species += [transcript] @@ -236,22 +751,59 @@ def update_reactions( transcript=None, leak=False, protein=None, - **keywords, + **kwargs, ): - """Update reactions for positive Hill transcription. - - This always requires the inputs component and part_id to find - the relevant parameters. - - :param dna: - :param regulator: - :param component: - :param part_id: - :param transcript: - :param leak: - :param protein: - :param keywords: - :return: + """Generate reactions for positive Hill transcription. + + Creates regulated transcription reaction(s) using a proportional + positive Hill function, with optional basal leak reaction. + + Parameters + ---------- + dna : Species + The DNA species (promoter) being transcribed. + regulator : Species + The activator species that regulates transcription. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup in the component's parameter + database. Required for parameter lookup. + transcript : Species, optional + The mRNA transcript species produced. If None and protein is + provided, protein is produced directly. + leak : bool, default=False + If True, includes a basal leak reaction. + protein : Species, optional + Protein species for expression mixtures. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing one or two reactions: + + - Regulated transcription with ProportionalHillPositive + propensity + - Optional leak reaction (if leak=True) + + Raises + ------ + AttributeError + If component or part_id is None (required for parameter lookup). + + Notes + ----- + The regulated reaction uses ProportionalHillPositive propensity: + + DNA --> DNA + Product (rate: k * G * R^n / (K + R^n)) + + Where Product is either transcript or protein depending on mixture + type. If leak=True, adds: + + DNA --> DNA + Product (rate: 'kleak') """ ktx = component.get_parameter('k', part_id=part_id, mechanism=self) @@ -294,14 +846,96 @@ def update_reactions( class NegativeHillTranscription(Mechanism): - """Model transcription as a proportional negative Hill function. + """Transcription regulated by negative Hill function (repression). - G --> G + P + A 'transcription' mechanism that models transcriptional repression using + a proportional negative Hill function. The transcription rate decreases + with regulator (repressor) concentration according to Hill kinetics, + capturing cooperative binding and repression. + + The reaction follows the schema: + + G --> G + T + + with rate: + + rate = k * G * 1 / (K + R^n) + + where R is the regulator (repressor), n is the Hill coefficient, and K + is the repression constant. Optionally includes a basal leak reaction at + rate 'kleak'. + + Parameters + ---------- + name : str, default='negativehill_transcription' + Name identifier for this mechanism instance. + mechanism_type : str, default='transcription' + Type classification of this mechanism. + + Attributes + ---------- + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + PositiveHillTranscription : Transcriptional activation with Hill + function. + SimpleTranscription : Simple transcription without regulation. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models transcriptional repression by regulatory proteins + such as repressor proteins. The Hill function captures the response + where increasing repressor concentration decreases transcription rate, + with cooperativity determined by the Hill coefficient. + + Key features: + + - Decreasing transcription with increasing repressor + - Cooperative repressor binding through Hill coefficient + - Optional basal transcription (leak) + - Saturation at high repressor concentrations + + Common applications include: + + - Repressible promoters + - Negative feedback loops + - Transcriptional repression cascades + - Genetic switches and oscillators + + Required parameters for this mechanism: + + - 'k' : Maximum transcription rate constant (when repressor is absent) + - 'K' : Repression constant (repressor concentration for half-maximal + repression) + - 'n' : Hill coefficient (cooperativity) + - 'kleak' : Basal transcription rate (optional, for leak reaction) + + Examples + -------- + Model transcriptional repression: + + >>> LacI = bcp.Protein('LacI') + >>> plac = bcp.RepressiblePromoter('plac', LacI) + >>> gene = bcp.DNAassembly( + ... name='repressed_GFP', + ... promoter=plac, rbs='RBS_medium', protein='GFP') + >>> tx_mechanism = bcp.NegativeHillTranscription() + >>> mixture = bcp.Mixture( + ... components=[LacI, gene], + ... mechanisms={ + ... 'transcription': tx_mechanism, + ... 'translation': bcp.SimpleTranslation(), + ... }, + ... parameters={'k': 1.0, 'K': 10.0, 'n': 2, 'kleak': 0.01}, + ... parameter_file='mixtures/pure_parameters.tsv', + ... ) + >>> mixture.compile_crn() - rate = k*G*(1)/(K+R^n) - where R is a regulator (repressor). - Optionally includes a leak reaction - G --> G + P @ rate kleak. """ def __init__( @@ -309,13 +943,6 @@ def __init__( name='negativehill_transcription', mechanism_type='transcription', ): - """Initializes a NegativeHillTranscription instance. - - :param name: name of the Mechanism, default: - negativehill_transcription - :param mechanism_type: type of the Mechanism, default: transcription - - """ Mechanism.__init__(self, name=name, mechanism_type=mechanism_type) def update_species( @@ -325,8 +952,35 @@ def update_species( transcript=None, leak=False, protein=None, - **keywords, + **kwargs, ): + """Generate species for negative Hill transcription. + + Returns all species involved in the regulated transcription + reaction. + + Parameters + ---------- + dna : Species + The DNA species (promoter) being transcribed. + regulator : Species + The repressor species that regulates transcription. + transcript : Species, optional + The mRNA transcript species produced. + leak : bool, default=False + If True, includes a leak reaction for basal transcription. + protein : Species, optional + Protein species for expression mixtures. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [dna, regulator] plus any non-None + transcript/protein species. + + """ species = [dna, regulator] if transcript is not None: species += [transcript] @@ -344,22 +998,46 @@ def update_reactions( transcript=None, leak=False, protein=None, - **keywords, + **kwargs, ): - """Update reactions for negative Hill transcription. - - This always requires the inputs component and part_id to find - the relevant parameters. - - :param dna: - :param regulator: - :param component: - :param part_id: - :param transcript: - :param leak: - :param protein: - :param keywords: - :return: + """Generate reactions for negative Hill transcription. + + Creates regulated transcription reaction(s) using a proportional + negative Hill function, with optional basal leak reaction. + + Parameters + ---------- + dna : Species + The DNA species (promoter) being transcribed. + regulator : Species + The repressor species that regulates transcription. + component : Component + Component containing parameter values. Required. + part_id : str + Identifier for parameter lookup. Required. + transcript : Species, optional + The mRNA transcript species produced. + leak : bool, default=False + If True, includes a basal leak reaction. + protein : Species, optional + Protein species for expression mixtures. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List containing one or two reactions: regulated transcription + with ProportionalHillNegative propensity, and optional leak + reaction. + + Notes + ----- + The regulated reaction uses ProportionalHillNegative propensity: + + DNA --> DNA + Product (rate: k * G / (K + R^n)) + + If leak=True, adds: DNA --> DNA + Product (rate: 'kleak') """ ktx = component.get_parameter('k', part_id=part_id, mechanism=self) @@ -402,17 +1080,88 @@ def update_reactions( class Transcription_MM(MichaelisMentenCopy): - """Michaelis Menten Transcription. + """Michaelis-Menten transcription with explicit RNA polymerase. + + A 'transcription' mechanism that explicitly models RNA polymerase (RNAP) + binding to DNA, followed by transcription and release. This mechanism + follows Michaelis-Menten kinetics and is appropriate when RNAP is + limiting or when explicit modeling of polymerase-DNA interactions is + needed. + + The reaction follows the schema: + + G + RNAP <--> G:RNAP --> G + RNAP + mRNA + + Parameters + ---------- + rnap : Species + RNA polymerase species that catalyzes transcription. + name : str, default='transcription_mm' + Name identifier for this mechanism instance. + + Attributes + ---------- + rnap : Species + The RNA polymerase species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + SimpleTranscription : Simple transcription without explicit RNAP. + Translation_MM : Michaelis-Menten translation. + MichaelisMentenCopy : Base class for MM copy mechanisms. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models transcription using standard Michaelis-Menten + kinetics where RNA polymerase acts as an enzyme that binds DNA, + catalyzes mRNA synthesis, and is released unchanged. This is appropriate + when: + + - RNAP concentration affects transcription rate + - Explicit modeling of RNAP-DNA binding is important + - RNAP sequestration or competition effects are relevant + + The mechanism uses the MichaelisMentenCopy base class which generates: + + 1. Reversible binding: DNA + RNAP <--> DNA:RNAP (rates: 'kb', 'ku') + 2. Catalysis: DNA:RNAP --> DNA + RNAP + mRNA (rate: 'ktx') + + Common applications include: + + - TX-TL systems with limited RNAP + - Models of transcriptional resource allocation + - Competition between promoters for RNAP + - Detailed mechanistic models of gene expression + + Required parameters for this mechanism: + + - 'ktx' : Transcription/catalysis rate constant + - 'kb' : Forward binding rate for RNAP to DNA + - 'ku' : Reverse unbinding rate for RNAP from DNA + + Examples + -------- + Model transcription with explicit RNA polymerase: + + >>> gene = bcp.DNAassembly( + ... name='gene_assembly', + ... promoter='pconst', transcript='mRNA') + >>> mechanism = bcp.Transcription_MM(rnap=rnap) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={'transcription': mechanism}, + ... parameters={'ktx': 0.05, 'kb': 1.0, 'ku': 0.1} + ... ) + >>> mixture.compile_crn() - G + RNAP <--> G:RNAP --> G+RNAP+mRNA """ - def __init__(self, rnap: Species, name='transcription_mm', **keywords): - """Initializes a Transcription_MM instance. - - :param rnap: Species instance that is representing an RNA polymerase - :param name: name of the Mechanism, default: transcription_mm - """ + def __init__(self, rnap: Species, name='transcription_mm', **kwargs): if isinstance(rnap, Species): self.rnap = rnap else: @@ -422,7 +1171,32 @@ def __init__(self, rnap: Species, name='transcription_mm', **keywords): self=self, name=name, mechanism_type='transcription' ) - def update_species(self, dna, transcript=None, protein=None, **keywords): + def update_species(self, dna, transcript=None, protein=None, **kwargs): + """Generate species for Michaelis-Menten transcription. + + Creates species involved in RNAP-mediated transcription including DNA, + RNAP, the DNA:RNAP complex, and transcript or protein product. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + transcript : Species, optional + The mRNA transcript species produced. If None and protein is + provided, protein is produced directly (for expression mixtures). + protein : Species, optional + Protein species for expression mixtures without explicit + transcription. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [dna, rnap, dna:rnap complex, product] where + product is either transcript or protein. + + """ species = [dna] if transcript is None and protein is not None: @@ -444,8 +1218,53 @@ def update_reactions( complex=None, transcript=None, protein=None, - **keywords, + **kwargs, ): + """Generate reactions for Michaelis-Menten transcription. + + Creates Michaelis-Menten transcription reactions including reversible + RNAP-DNA binding and catalytic transcript production. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Complex, optional + Pre-specified DNA:RNAP complex species. If None, automatically + created. + transcript : Species, optional + The mRNA transcript species produced. If None and protein is + provided, protein is produced directly. + protein : Species, optional + Protein species for expression mixtures. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of two reactions: + + - Reversible RNAP-DNA binding (rates: 'kb', 'ku') + - Catalytic transcription (rate: 'ktx') + + Notes + ----- + The reactions follow the Michaelis-Menten scheme: + + 1. DNA + RNAP <--> DNA:RNAP (rates: 'kb' and 'ku') + 2. DNA:RNAP --> DNA + RNAP + Product (rate: 'ktx') + + Where Product is either transcript or protein depending on mixture + type. + + """ # Get Parameters if part_id is None and component is not None: part_id = component.name @@ -476,17 +1295,90 @@ def update_reactions( class Translation_MM(MichaelisMentenCopy): - """Michaelis Menten Translation. + """Michaelis-Menten translation with explicit ribosome. + + A 'translation' mechanism that explicitly models ribosome binding to + mRNA, followed by translation and release. This mechanism follows + Michaelis-Menten kinetics and is appropriate when ribosomes are limiting + or when explicit modeling of ribosome-mRNA interactions is needed. + + The reaction follows the schema: + + mRNA + Rib <--> mRNA:Rib --> mRNA + Rib + Protein + + Parameters + ---------- + ribosome : Species + Ribosome species that catalyzes translation. + name : str, default='translation_mm' + Name identifier for this mechanism instance. + + Attributes + ---------- + ribosome : Species + The ribosome species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('translation'). + + See Also + -------- + SimpleTranslation : Simple translation without explicit ribosomes. + Transcription_MM : Michaelis-Menten transcription. + MichaelisMentenCopy : Base class for MM copy mechanisms. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism models translation using standard Michaelis-Menten + kinetics where ribosomes act as enzymes that bind mRNA, catalyze + protein synthesis, and are released unchanged. This is appropriate when: + + - Ribosome concentration affects translation rate + - Explicit modeling of ribosome-mRNA binding is important + - Ribosome sequestration or competition effects are relevant + + The mechanism uses the MichaelisMentenCopy base class which generates: + + 1. Reversible binding: mRNA + Rib <--> mRNA:Rib (rates: 'kb', 'ku') + 2. Catalysis: mRNA:Rib --> mRNA + Rib + Protein (rate: 'ktl') + + Common applications include: + + - TX-TL systems with limited ribosomes + - Models of translational resource allocation + - Competition between mRNAs for ribosomes + - Detailed mechanistic models of protein expression + + Required parameters for this mechanism: + + - 'ktl' : Translation/catalysis rate constant + - 'kb' : Forward binding rate for ribosome to mRNA + - 'ku' : Reverse unbinding rate for ribosome from mRNA + + Examples + -------- + Model translation with explicit ribosomes: + + >>> gene = bcp.DNAassembly( + ... name='gene_assembly', + ... promoter='pconst', transcript='mRNA', + ... rbs='RBS_medium', protein='GFP') + >>> ribosome = bcp.Species('Ribosome', material_type='protein') + >>> tl_mechanism = bcp.Translation_MM(ribosome=ribosome) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': mechanism}, + ... parameters={'ktx': 0.05, 'ktl': 0.1, 'kb': 1.0, 'ku': 0.1} + ... ) + >>> mixture.compile_crn() - mRNA + Rib <--> mRNA:Rib --> mRNA + Rib + Protein """ - def __init__(self, ribosome: Species, name='translation_mm', **keywords): - """Initializes a Translation_MM instance. - - :param ribosome: Species instance that is representing a ribosome - :param name: name of the Mechanism, default: translation_mm - """ + def __init__(self, ribosome: Species, name='translation_mm', **kwargs): if isinstance(ribosome, Species): self.ribosome = ribosome else: @@ -495,7 +1387,31 @@ def __init__(self, ribosome: Species, name='translation_mm', **keywords): self=self, name=name, mechanism_type='translation' ) - def update_species(self, transcript, protein, **keywords): + def update_species(self, transcript, protein, **kwargs): + """Generate species for Michaelis-Menten translation. + + Creates species involved in ribosome-mediated translation including + mRNA, ribosome, the mRNA:ribosome complex, and protein product. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. Can be None in + expression mixtures without explicit transcription. + protein : Species + The protein species produced by translation. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing translation species. In expression mixtures + without transcription (transcript is None), returns [protein]. + Otherwise returns [ribosome, transcript, mRNA:ribosome complex, + protein]. + + """ species = [] # This can only occur in expression mixtures @@ -518,8 +1434,53 @@ def update_reactions( component, part_id=None, complex=None, - **keywords, + **kwargs, ): + """Generate reactions for Michaelis-Menten translation. + + Creates Michaelis-Menten translation reactions including reversible + ribosome-mRNA binding and catalytic protein production. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. Can be None in + expression mixtures. + protein : Species + The protein species produced by translation. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Complex, optional + Pre-specified mRNA:ribosome complex species. If None, + automatically created. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of two reactions for translation if transcript is not None: + + - Reversible ribosome-mRNA binding (rates: 'kb', 'ku') + - Catalytic translation (rate: 'ktl') + + Returns empty list if transcript is None (expression mixtures). + + Notes + ----- + The reactions follow the Michaelis-Menten scheme: + + 1. mRNA + Ribosome <--> mRNA:Ribosome (rates: 'kb' and 'ku') + 2. mRNA:Ribosome --> mRNA + Ribosome + Protein (rate: 'ktl') + + In expression mixtures without explicit transcription, no translation + reactions are generated (empty list returned). + + """ rxns = [] # Get Parameters @@ -548,18 +1509,107 @@ def update_reactions( class Energy_Transcription_MM(Mechanism): - """Michaelis Menten Transcription that consumed energy. + """Michaelis-Menten transcription with explicit energy consumption. - G + RNAP <--> G:RNAP - Fuel + G:RNAP --> G + RNAP + T + Fuel + A 'transcription' mechanism that models transcription with explicit + consumption of energy sources (fuel species like NTPs) and production of + waste products. This mechanism couples RNAP-DNA binding with + length-dependent fuel consumption to model realistic transcription + energetics. - Transcription can only happen when there is fuel, at rate ktx/L - (length dependent transcription rate) + The reaction follows the schema: + G + RNAP <--> G:RNAP + Fuel + G:RNAP --> G + RNAP + T Fuel + G:RNAP --> G:RNAP + wastes - Fuel consumption treated faster at rate ktx. This occurs L times - faster than the above, resulting in the correct fuel use. + Transcription occurs at rate 'ktx' / L (length-dependent), while fuel + consumption occurs at rate 'ktx', resulting in L times more fuel + consumption than transcripts produced. + + Parameters + ---------- + rnap : Species + RNA polymerase species that catalyzes transcription. + fuels : list of Species + List of fuel species (e.g., NTPs) consumed during transcription. + wastes : list of Species + List of waste species (e.g., pyrophosphate) produced during + transcription. + name : str, default='energy_transcription_mm' + Name identifier for this mechanism instance. + + Attributes + ---------- + rnap : Species + The RNA polymerase species. + fuels : list of Species + Fuel species consumed during transcription. + wastes : list of Species + Waste species produced during transcription. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + Transcription_MM : MM transcription without explicit energy. + MichaelisMentenCopy : Base class for MM copy mechanisms. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism provides a more realistic model of transcription by + explicitly tracking energy consumption. Key features: + + - Length-dependent transcription rate ('ktx' / L) + - Explicit fuel (NTP) consumption + - Waste product generation + - RNAP-DNA binding dynamics + + The length parameter L represents the gene length in appropriate units, + and the mechanism automatically scales fuel consumption to match the + energetic requirements of synthesizing an mRNA of length L. + + Common applications include: + + - Detailed TX-TL models with explicit resources + - Models of transcriptional burden and resource depletion + - Systems where NTP availability affects gene expression + + Required parameters for this mechanism: + + - 'ktx' : Base transcription rate constant + - 'kb' : Forward binding rate for RNAP to DNA + - 'ku' : Reverse unbinding rate for RNAP from DNA + - 'length' : Gene length (for length-dependent transcription) + + Examples + -------- + Model transcription with explicit NTP consumption: + + >>> gene = bcp.DNAassembly( + ... name='constitutive_promoter', + ... promoter='pconst', rbs='RBS_medium', protein='GFP') + >>> rnap = bcp.Species('RNAP') + >>> ntp = bcp.Species('NTP') + >>> ppi = bcp.Species('PPi') + >>> mechanism = bcp.Energy_Transcription_MM( + ... rnap=rnap, + ... fuels=[ntp], + ... wastes=[ppi] + ... ) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': mechanism, + ... 'translation': bcp.SimpleTranslation() + ... }, + ... parameters={'ktx': 0.05, 'kb': 1.0, 'ku': 0.1, 'length': 1000}, + ... parameter_file='mixtures/pure_parameters.tsv' + ... ) + >>> mixture.compile_crn() """ @@ -567,17 +1617,10 @@ def __init__( self, rnap: Species, fuels: List[Species], - wastes=List[Species], + wastes: List[Species], name='energy_transcription_mm', - **keywords, + **kwargs, ): - """Initializes a Transcription_MM instance. - - :param fuels: List of Species consumed during transcription - :param wastes: List of Species consumed during transcription - :param rnap: Species instance that is representing an RNA polymerase - :param name: name of the Mechanism, default: transcription_mm - """ if isinstance(rnap, Species): self.rnap = rnap else: @@ -599,7 +1642,32 @@ def __init__( self=self, name=name, mechanism_type='transcription' ) - def update_species(self, dna, transcript=None, protein=None, **keywords): + def update_species(self, dna, transcript=None, protein=None, **kwargs): + """Generate species for energy-consuming transcription. + + Creates species involved in transcription with explicit energy + consumption including DNA, RNAP, the DNA:RNAP complex, transcript, + and fuel species. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + transcript : Species, optional + The mRNA transcript species produced. + protein : Species, optional + Protein species (unused in this mechanism, accepted for API + consistency). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [dna, rnap, transcript, fuels..., dna:rnap + complex]. + + """ species = [dna, self.rnap, transcript] + self.fuels bound_complex = Complex([dna, self.rnap]) species += [bound_complex] @@ -614,8 +1682,56 @@ def update_reactions( complex=None, transcript=None, protein=None, - **keywords, + **kwargs, ): + """Generate reactions for energy-consuming transcription. + + Creates three reactions modeling transcription with explicit fuel + consumption and waste production: RNAP-DNA binding, length-dependent + transcription, and fuel consumption. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Complex, optional + Pre-specified DNA:RNAP complex species (unused, complex is + created internally). + transcript : Species, optional + The mRNA transcript species produced. + protein : Species, optional + Protein species (unused, accepted for API consistency). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of three reactions: + + - RNAP-DNA binding (rates: 'kb', 'ku') + - Transcription with fuel (rate: 'ktx' / 'length') + - Fuel consumption producing wastes (rate: 'ktx') + + Notes + ----- + The reactions model transcription energetics: + + 1. DNA + RNAP <--> DNA:RNAP (rates: 'kb' and 'ku') + 2. Fuel + DNA:RNAP --> Fuel + DNA + RNAP + mRNA (rate: 'ktx' / L) + 3. Fuel + DNA:RNAP --> DNA:RNAP + Wastes (rate: 'ktx') + + The length-dependent transcription rate ('ktx' / L) ensures that L + times more fuel is consumed than transcripts produced, reflecting the + energetic cost of synthesizing a transcript of length L. + + """ # Get Parameters if part_id is None and component is not None: part_id = component.name @@ -648,11 +1764,109 @@ def update_reactions( class Energy_Translation_MM(Mechanism): - """Michaelis Menten Translation that consumes energy species. + """Michaelis-Menten translation with explicit energy consumption. + + A 'translation' mechanism that models translation with explicit + consumption of energy sources (fuel species like amino acids/NTPs) and + production of waste products. This mechanism couples ribosome-mRNA binding + with length-dependent fuel consumption to model realistic translation + energetics. + + The reaction follows the schema: + + mRNA + Rib <--> mRNA:Rib + Fuel + mRNA:Rib --> mRNA + Rib + Protein + Fuel + Fuel + mRNA:Rib --> mRNA:Rib + wastes + + Translation occurs at rate 'ktl' / L (length-dependent), while fuel + consumption occurs at rate 'ktl', resulting in L times more fuel + consumption than proteins produced. + + Parameters + ---------- + ribosome : Species + Ribosome species that catalyzes translation. + fuels : list of Species + List of fuel species (e.g., amino acids, GTP) consumed during + translation. + wastes : list of Species + List of waste species produced during translation. + name : str, default='energy_translation_mm' + Name identifier for this mechanism instance. + + Attributes + ---------- + ribosome : Species + The ribosome species. + fuels : list of Species + Fuel species consumed during translation. + wastes : list of Species + Waste species produced during translation. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('translation'). + + See Also + -------- + Translation_MM : MM translation without explicit energy. + Energy_Transcription_MM : MM transcription with explicit energy. + MichaelisMentenCopy : Base class for MM copy mechanisms. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism provides a more realistic model of translation by + explicitly tracking energy consumption. Key features: + + - Length-dependent translation rate ('ktl' / L) + - Explicit fuel (amino acid/NTP) consumption + - Waste product generation + - Ribosome-mRNA binding dynamics + + The length parameter L represents the gene/protein length in appropriate + units, and the mechanism automatically scales fuel consumption to match + the energetic requirements of synthesizing a protein of length L. + + Common applications include: + + - Detailed TX-TL models with explicit resources + - Models of translational burden and resource depletion + - Systems where amino acid availability affects protein expression + + Required parameters for this mechanism: + + - 'ktl' : Base translation rate constant + - 'kb' : Forward binding rate for ribosome to mRNA + - 'ku' : Reverse unbinding rate for ribosome from mRNA + - 'length' : Gene/protein length (for length-dependent translation) + + Examples + -------- + Model translation with explicit amino acid consumption: + + >>> gene = bcp.DNAassembly( + ... name='constitutive_promoter', + ... promoter='pconst', rbs='RBS_medium', protein='GFP') + >>> ribosome = bcp.Species('Ribosome') + >>> amino_acids = bcp.Species('AA') + >>> waste = bcp.Species('waste') + >>> mechanism = bcp.Energy_Translation_MM( + ... ribosome=ribosome, + ... fuels=[amino_acids], + ... wastes=[waste] + ... ) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': mechanism, + ... }, + ... parameters={'ktl': 0.1, 'kb': 1.0, 'ku': 0.1, 'length': 300}, + ... parameter_file='mixtures/pure_parameters.tsv' + ... ) + >>> mixture.compile_crn() - mRNA + Rib <--> mRNA:Rib (binding) - fuels + mRNA:Rib --> mRNA + Rib + Protein + fuels (translation) - fuels + mRNA:Rib --> mRNA:Rib +wastes (fuel consumption) """ def __init__( @@ -661,16 +1875,8 @@ def __init__( fuels: List[Species], wastes=List[Species], name='energy_translation_mm', - **keywords, + **kwargs, ): - """Initializes a Translation_MM instance. - - :param ribosome: Species instance that is representing a ribosome - :param fuels: List of fuel Species that are consumed during - translation - :param wastes: List of Species consumed during translation - :param name: name of the Mechanism, default: energy_translation_mm - """ if isinstance(ribosome, Species): self.ribosome = ribosome else: @@ -688,7 +1894,29 @@ def __init__( Mechanism.__init__(self=self, name=name, mechanism_type='translation') - def update_species(self, transcript, protein, **keywords): + def update_species(self, transcript, protein, **kwargs): + """Generate species for energy-consuming translation. + + Creates species involved in translation with explicit energy + consumption including mRNA, ribosome, the mRNA:ribosome complex, + protein, and fuel species. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. + protein : Species + The protein species produced by translation. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing [fuels..., ribosome, protein, mRNA:ribosome + complex]. + + """ species = self.fuels + [self.ribosome, protein] bound_complex = Complex([transcript, self.ribosome]) species += [bound_complex] @@ -702,8 +1930,55 @@ def update_reactions( component, part_id=None, complex=None, - **keywords, + **kwargs, ): + """Generate reactions for energy-consuming translation. + + Creates three reactions modeling translation with explicit fuel + consumption and waste production: ribosome-mRNA binding, + length-dependent translation, and fuel consumption. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. + protein : Species + The protein species produced by translation. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str, optional + Identifier for parameter lookup. If None, defaults to + component.name. + complex : Complex, optional + Pre-specified mRNA:ribosome complex species (unused, complex is + created internally). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of three reactions: + + - Ribosome-mRNA binding (rates: 'kb', 'ku') + - Translation with fuel (rate: 'ktl' / 'length') + - Fuel consumption producing wastes (rate: 'ktl') + + Notes + ----- + The reactions model translation energetics: + + 1. mRNA + Ribosome <--> mRNA:Ribosome (rates: 'kb' and 'ku') + 2. Fuel + mRNA:Ribosome --> Fuel + mRNA + Ribosome + Protein (rate: + 'ktl' / L) + 3. Fuel + mRNA:Ribosome --> mRNA:Ribosome + Wastes (rate: 'ktl') + + The length-dependent translation rate ('ktl' / L) ensures that L + times more fuel is consumed than proteins produced, reflecting the + energetic cost of synthesizing a protein of length L. + + """ # Get Parameters if part_id is None and component is not None: part_id = component.name @@ -739,24 +2014,102 @@ def update_reactions( class multi_tx(Mechanism): - """Multi-RNAp Transcription w/ Isomerization. + """Multi-polymerase transcription with isomerization and occupancy. - Detailed transcription mechanism accounting for each individual - RNAp occupancy states of gene. + A 'transcription' mechanism that explicitly models multiple RNA + polymerases (RNAPs) binding to a single gene simultaneously, accounting + for polymerase spacing, isomerization between open and closed + configurations, and competitive binding. This detailed mechanism captures + transcriptional queueing and interference effects. - n ={0, max_occ} - DNA:RNAp_n + RNAp <--> DNA:RNAp_n_c --> DNA:RNAp_n+1 - DNA:RNAp_n --> DNA:RNAp_0 + n RNAp + n mRNA - DNA:RNAp_n_c --> DNA:RNAp_0_c + n RNAp + n mRNA + The reaction scheme follows: + DNA:RNAp_n + RNAp <--> DNA:RNAp_n_c --> DNA:RNAp_n+1 + DNA:RNAp_n --> DNA:RNAp_0 + n RNAp + n mRNA + DNA:RNAp_n_c --> DNA:RNAp_0_c + n RNAp + n mRNA - n --> number of open configuration RNAp on DNA - max_occ --> Physical maximum number of RNAp on DNA (based on - RNAp and DNA dimensions) - DNA:RNAp_n --> DNA with n open configuration RNAp on it - DNA:RNAp_n_c --> DNA with n open configuration RNAp and 1 closed - configuration RNAp on it + where: + + - n = {0, 1, ..., max_occ} is the number of polymerases + - max_occ is the physical maximum based on RNAP and DNA dimensions + - DNA:RNAp_n represents DNA with n open configuration RNAPs + - DNA:RNAp_n_c represents DNA with n open RNAPs and 1 closed RNAP + + Parameters + ---------- + pol : Species + RNA polymerase species. + name : str, default='multi_tx' + Name identifier for this mechanism instance. + mechanism_type : str, default='transcription' + Type classification of this mechanism. + + Attributes + ---------- + pol : Species + The RNA polymerase species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('transcription'). + + See Also + -------- + Transcription_MM : Simple Michaelis-Menten transcription. + multi_tl : Multi-ribosome translation mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism provides a detailed, spatially-aware model of + transcription that captures: + + - Multiple polymerases on a single gene + - Polymerase queueing and spacing constraints + - Open/closed conformational states (isomerization) + - Coordinated transcript release from multiple polymerases + + The model is appropriate for detailed mechanistic studies where: + + - Polymerase density and spacing matter + - Transcriptional queueing affects expression + - Multiple concurrent transcription events are important + + The mechanism generates many species (2 * max_occ complexes) and + reactions (O(max_occ)), so it should be used with caution for + computational efficiency. + + Required parameters for this mechanism: + + - 'ktx' : Transcription/release rate constant + - 'kb' : Forward binding rate for polymerase to DNA + - 'ku' : Reverse unbinding rate for polymerase from DNA + - 'k_iso' : Isomerization rate from closed to open configuration + - 'max_occ' : Maximum polymerase occupancy (integer) + + Examples + -------- + Model multi-polymerase transcription: + + >>> gene = bcp.DNAassembly( + ... name='dna_assembly', + ... promoter='pconst', rbs='RBS_medium', protein='GFP') + >>> rnap = bcp.Species('RNAP') + >>> tx_mechanism = bcp.multi_tx(pol=rnap) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': tx_mechanism, + ... 'translation': bcp.SimpleTranslation() + ... }, + ... parameters={ + ... 'ktx': 0.05, 'ktl': 0.1, 'kb': 1.0, 'ku': 0.1, + ... 'k_iso': 0.5, 'max_occ': 5 + ... } + ... ) + >>> mixture.compile_crn() + + For more details, see examples/MultiTX_Demo.ipynb. - For more details, see examples/MultiTX_Demo.ipynb """ def __init__( @@ -764,16 +2117,8 @@ def __init__( pol: Species, name: str = 'multi_tx', mechanism_type: str = 'transcription', - **keywords, + **kwargs, ): - """Initializes a multi_tx instance. - - :param pol: reference to a species instance that represents a - polymerase - :param name: name of the Mechanism, default: multi_tx - :param mechanism_type: type of the mechanism, default: transcription - :param keywords: - """ if isinstance(pol, Species): self.pol = pol else: @@ -783,8 +2128,54 @@ def __init__( # species update def update_species( - self, dna, transcript, component, part_id, protein=None, **keywords + self, dna, transcript, component, part_id, protein=None, **kwargs ): + """Generate species for multi-polymerase transcription. + + Creates all species and complexes involved in multi-polymerase + transcription including DNA with varying numbers of bound polymerases + in both open and closed configurations. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + transcript : Species + The mRNA transcript species produced. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup. Required to retrieve 'max_occ' + parameter. + protein : Species, optional + Protein species (unused, accepted for API consistency). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing: + + - DNA:RNAP_n complexes in open configuration (n = 0 to max_occ-1) + - DNA:RNAP_n complexes in closed configuration (n = 0 to + max_occ-1) + - Individual species [polymerase, dna, transcript] + + Notes + ----- + For each occupancy level n from 0 to max_occ-1, two complex species + are created: + + - Open configuration: DNA with n+1 polymerases, all in open state + - Closed configuration: DNA with n+1 polymerases (n open, 1 closed) + + The 'max_occ' parameter determines the maximum number of polymerases + that can occupy the gene simultaneously, typically based on physical + spacing constraints. + + """ max_occ = int( component.get_parameter( 'max_occ', @@ -814,18 +2205,56 @@ def update_species( return cp_open + cp_closed + cp_misc def update_reactions( - self, dna, transcript, component, part_id, protein=None, **keywords + self, dna, transcript, component, part_id, protein=None, **kwargs ): - """It sets up the following reactions. + """Generate reactions for multi-polymerase transcription. + + Creates reactions modeling multiple polymerases binding to DNA, + isomerization between configurations, and transcript release with + coordination among multiple polymerases. + + Parameters + ---------- + dna : Species + The DNA species (gene) being transcribed. + transcript : Species + The mRNA transcript species produced. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup. Required to retrieve parameters. + protein : Species, optional + Protein species (unused, accepted for API consistency). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of reactions including: + + - Polymerase binding reactions (DNA:RNAP_n + RNAP <--> + DNA:RNAP_n_c) + - Isomerization reactions (DNA:RNAP_n_c --> DNA:RNAP_n) + - Release reactions from open states (DNA:RNAP_n --> DNA + n*RNAP + + n*mRNA) + - Release reactions from closed states (DNA:RNAP_n_c --> + DNA:RNAP_0_c + n*RNAP + n*mRNA) + - Base binding reaction (DNA + RNAP <--> DNA:RNAP_0_c) + + Notes + ----- + The reaction scheme captures: + + 1. DNA:RNAP_n + RNAP <--> DNA:RNAP_(n+1)_c (rates: 'kb', 'ku') + 2. DNA:RNAP_n_c --> DNA:RNAP_n (rate: 'k_iso') + 3. DNA:RNAP_n --> DNA + n*RNAP + n*mRNA (rate: 'ktx') + 4. DNA:RNAP_n_c --> DNA:RNAP_0_c + n*RNAP + n*mRNA (rate: 'ktx') + + Where n ranges from 0 to max_occ-1. The 'max_occ' parameter + represents the physical maximum occupancy of polymerases on the gene. - DNA:RNAp_n + RNAp <--> DNA:RNAp_n_c --> DNA:RNAp_n+1 - kf1 = k1, kr1 = k2, kf2 = k_iso - DNA:RNAp_n --> DNA:RNAp_0 + n RNAp + n mRNA - kf = ktx_solo - DNA:RNAp_n_c --> DNA:RNAp_0_c + n RNAp + n mRNA - kf = ktx_solo - - max_occ = maximum occupancy of gene (physical limit) """ # parameter loading kb = component.get_parameter('kb', part_id=part_id, mechanism=self) @@ -932,25 +2361,106 @@ def update_reactions( class multi_tl(Mechanism): - """Multi-RBZ Translation w/ Isomerization. + """Multi-ribosome translation with isomerization and occupancy. - Detailed translation mechanism accounting for each individual - RBZ occupancy states of mRNA. Still needs some work, so use with caution, - read all warnings and consult the example notebook. + A 'translation' mechanism that explicitly models multiple ribosomes + binding to a single mRNA simultaneously, accounting for ribosome spacing, + isomerization between open and closed configurations, and competitive + binding. This detailed mechanism captures translational queueing and + ribosome traffic effects. - n ={0, max_occ} - mRNA:RBZ_n + RBZ <--> mRNA:RBZ_n_c --> mRNA:RBZ_n+1 - mRNA:RBZ_n --> mRNA:RBZ_0 + n RBZ + n Protein - mRNA:RBZ_n_c --> mRNA:RBZ_0_c + n RBZ + n Protein + The reaction scheme follows: + mRNA:RBZ_n + RBZ <--> mRNA:RBZ_n_c --> mRNA:RBZ_n+1 + mRNA:RBZ_n --> mRNA:RBZ_0 + n RBZ + n Protein + mRNA:RBZ_n_c --> mRNA:RBZ_0_c + n RBZ + n Protein - n --> number of open configuration RBZ on mRNA - max_occ --> Physical maximum number of RBZ on mRNA - (based on RBZ and mRNA dimensions) - mRNA:RBZ_n --> mRNA with n open configuration RBZ on it - mRNA:RBZ_n_c --> mRNA with n open configuration RBZ and 1 - closed configuration RBZ on it + where: + + - n = {0, 1, ..., max_occ} is the number of ribosomes + - max_occ is the physical maximum based on ribosome and mRNA dimensions + - mRNA:RBZ_n represents mRNA with n open configuration ribosomes + - mRNA:RBZ_n_c represents mRNA with n open and 1 closed ribosome + + Parameters + ---------- + ribosome : Species + Ribosome species. + name : str, default='multi_tl' + Name identifier for this mechanism instance. + mechanism_type : str, default='translation' + Type classification of this mechanism. + + Attributes + ---------- + ribosome : Species + The ribosome species. + name : str + Name of the mechanism instance. + mechanism_type : str + Type classification ('translation'). + + See Also + -------- + Translation_MM : Simple Michaelis-Menten translation. + multi_tx : Multi-polymerase transcription mechanism. + Mechanism : Base class for all mechanisms. + + Notes + ----- + This mechanism provides a detailed, spatially-aware model of translation + that captures: + + - Multiple ribosomes on a single mRNA (polysome formation) + - Ribosome queueing and spacing constraints + - Open/closed conformational states (isomerization) + - Coordinated protein release from multiple ribosomes + + The model is appropriate for detailed mechanistic studies where: + + - Ribosome density and spacing matter + - Translational queueing affects expression + - Multiple concurrent translation events are important + - Polysome dynamics are relevant + + The mechanism generates many species (2 * max_occ complexes) and + reactions (O(max_occ)), so it should be used with caution for + computational efficiency. + + CAUTION: This mechanism is still under development. Use with care, read + all warnings, and consult the example notebook before use. + + Required parameters for this mechanism: + + - 'ktl' : Translation/release rate constant + - 'kb' : Forward binding rate for ribosome to mRNA + - 'ku' : Reverse unbinding rate for ribosome from mRNA + - 'k_iso' : Isomerization rate from closed to open configuration + - 'max_occ' : Maximum ribosome occupancy (integer) + + Examples + -------- + Model multi-ribosome translation: + + >>> gene = bcp.DNAassembly( + ... name='gene_assembly', + ... promoter='pconst', transcript='mRNA', + ... rbs='RBS_medium', protein='GFP') + >>> ribosome = bcp.Species('Ribosome') + >>> tl_mechanism = bcp.multi_tl(ribosome=ribosome) + >>> mixture = bcp.Mixture( + ... components=[gene], + ... mechanisms={ + ... 'transcription': bcp.SimpleTranscription(), + ... 'translation': tl_mechanism}, + ... parameters={ + ... 'ktx': 0.05, 'ktl': 0.1, 'kb': 1.0, 'ku': 0.1, + ... 'k_iso': 0.5, 'max_occ': 5 + ... } + ... ) + >>> mixture.compile_crn() + + For more details, see examples/MultiTX_Demo.ipynb. - For more details, see examples/MultiTX_Demo.ipynb """ def __init__( @@ -958,15 +2468,8 @@ def __init__( ribosome: Species, name: str = 'multi_tl', mechanism_type: str = 'translation', - **keywords, + **kwargs, ): - """Initializes a multi_tl instance. - - :param ribosome: a Species instance that represents a ribosome - :param name: name of the Mechanism, default: multi_tl - :param mechanism_type: type of the Mechanism, default: translation - - """ if isinstance(ribosome, Species): self.ribosome = ribosome else: @@ -976,8 +2479,53 @@ def __init__( # species update def update_species( - self, transcript, protein, component, part_id, **keywords + self, transcript, protein, component, part_id, **kwargs ): + """Generate species for multi-ribosome translation. + + Creates all species and complexes involved in multi-ribosome + translation including mRNA with varying numbers of bound ribosomes in + both open and closed configurations. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. + protein : Species + The protein species produced by translation. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup. Required to retrieve 'max_occ' + parameter. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Species + List containing: + + - mRNA:Ribosome_n complexes in open configuration (n = 0 to + max_occ-1) + - mRNA:Ribosome_n complexes in closed configuration (n = 0 to + max_occ-1) + - Individual species [ribosome, transcript, protein] + + Notes + ----- + For each occupancy level n from 0 to max_occ-1, two complex species + are created: + + - Open configuration: mRNA with n+1 ribosomes, all in open state + - Closed configuration: mRNA with n+1 ribosomes (n open, 1 closed) + + The 'max_occ' parameter determines the maximum number of ribosomes + that can occupy the mRNA simultaneously, typically based on physical + spacing constraints and mRNA length. + + """ max_occ = int( component.get_parameter( 'max_occ', @@ -1007,16 +2555,55 @@ def update_species( return cp_open + cp_closed + cp_misc def update_reactions( - self, transcript, protein, component, part_id, **keywords + self, transcript, protein, component, part_id, **kwargs ): - """It sets up the following reactions. + """Generate reactions for multi-ribosome translation. + + Creates reactions modeling multiple ribosomes binding to mRNA, + isomerization between configurations, and protein release with + coordination among multiple ribosomes. + + Parameters + ---------- + transcript : Species + The mRNA transcript species being translated. + protein : Species + The protein species produced by translation. + component : Component + Component containing parameter values. Required for parameter + lookup. + part_id : str + Identifier for parameter lookup. Required to retrieve parameters. + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + list of Reaction + List of reactions including: + + - Ribosome binding reactions (mRNA:RBZ_n + RBZ <--> + mRNA:RBZ_n_c) + - Isomerization reactions (mRNA:RBZ_n_c --> mRNA:RBZ_n) + - Release reactions from open states (mRNA:RBZ_n --> mRNA + n*RBZ + + n*Protein) + - Release reactions from closed states (mRNA:RBZ_n_c --> + mRNA:RBZ_0_c + n*RBZ + n*Protein) + - Base binding reaction (mRNA + RBZ <--> mRNA:RBZ_0_c) + + Notes + ----- + The reaction scheme captures: + + 1. mRNA:RBZ_n + RBZ <--> mRNA:RBZ_(n+1)_c (rates: 'kb', 'ku') + 2. mRNA:RBZ_n_c --> mRNA:RBZ_n (rate: 'k_iso') + 3. mRNA:RBZ_n --> mRNA + n*RBZ + n*Protein (rate: 'ktl') + 4. mRNA:RBZ_n_c --> mRNA:RBZ_0_c + n*RBZ + n*Protein (rate: 'ktl') + + Where n ranges from 0 to max_occ-1. The 'max_occ' parameter + represents the physical maximum occupancy of ribosomes on the mRNA, + determined by ribosome spacing and mRNA length. - mRNA:RBZ_n + RBZ <--> mRNA:RBZ_n_c --> mRNA:RBZ_n+1 - kf1 = kbr, kr1 = kur, kf2 = k_iso_r - mRNA:RBZ_n --> mRNA:RBZ_0 + n RBZ + n Protein - kf = ktl_solo - mRNA:RBZ_n_c --> mRNA:RBZ_0_c + n RBZ + n Protein - kf = ktl_solo """ # parameter loading kb = component.get_parameter('kb', part_id=part_id, mechanism=self) diff --git a/biocrnpyler/mixtures/__init__.py b/biocrnpyler/mixtures/__init__.py index 16e30875..61d83034 100644 --- a/biocrnpyler/mixtures/__init__.py +++ b/biocrnpyler/mixtures/__init__.py @@ -1,9 +1,9 @@ """BioCRNpyler mixture library. -A Mixture in BioCRNpyler defines the *context* in which Components are +A mixture in BioCRNpyler defines the *context* in which components are compiled into a chemical reaction network (CRN). A mixture ties together components, mechanisms, and parameters by specifying *which* -Mechanisms are available, *which* Components are present, and *what* +Mechanisms are available, *which* components are present, and *what* parameters to use. """ diff --git a/biocrnpyler/mixtures/cell.py b/biocrnpyler/mixtures/cell.py index 1b88c71c..07484166 100644 --- a/biocrnpyler/mixtures/cell.py +++ b/biocrnpyler/mixtures/cell.py @@ -19,20 +19,146 @@ class ExpressionDilutionMixture(Mixture): - """In vivo gene expression without any machinery. - - Here transcription and translation are lumped into one reaction: - expression, without RNA polymerase or ribosomes. A global mechanism is - used to dilute all non-DNA species. + """In vivo gene expression with dilution but without cellular machinery. + + A simplified mixture that models gene expression as a single direct + reaction from DNA to protein, without explicitly representing + transcription and translation as separate processes or cellular machinery + (ribosomes, polymerases). This mixture lumps transcription and + translation into a single 'expression' reaction and includes global + dilution to model cell growth and division effects on all non-DNA + species. + + This mixture is appropriate for coarse-grained models of in vivo gene + expression where mRNA dynamics are negligible and growth dilution is + important. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + ExpressionExtract : Expression without dilution. + SimpleTxTlDilutionMixture : TX-TL with dilution. + TxTlDilutionMixture : TX-TL with machinery and dilution. + Mixture : Base class for all mixtures. + + Notes + ----- + Default mechanisms included: + + - 'transcription' : `OneStepGeneExpression` - Single-step gene + expression (DNA --> DNA + Protein) without intermediate mRNA + - 'translation' : `EmptyMechanism` - Dummy mechanism that generates no + reactions (translation is disabled) + - 'catalysis' : `BasicCatalysis` - Simple catalytic reactions without + explicit enzyme binding + - 'binding' : `One_Step_Binding` - Simple multi-species binding + - 'dilution' : `Dilution` - Global dilution mechanism (Species --> ∅) + applied to all non-DNA species to model growth/division + + Key features of this mixture: + + - No explicit transcription or translation steps + - No cellular machinery (RNAP, ribosomes, RNases) + - No intermediate mRNA species + - Global dilution of all species except DNA + - Models growth dilution effects in vivo + - Simplified parameter space + + When compiled, this mixture automatically disables transcript generation + in DNAassemblies that produce proteins, routing expression directly from + DNA to protein. + + Common applications include: + + - In vivo gene circuit modeling with growth effects + - Steady-state gene expression in growing cells + - Models where mRNA dynamics are negligible + - High-level circuit design with dilution + + Examples + -------- + Create an in vivo expression mixture with dilution for GFP: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... protein='GFP' + ... ) + >>> mixture = bcp.ExpressionDilutionMixture( + ... name='cell_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/cell_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ def __init__(self, name='', **kwargs): - """Initializes an ExpressionDilutionMixture instance. - - :param name: name of the mixture - :param kwargs: keywords passed into the parent Class (Mixture) - """ Mixture.__init__(self, name=name, **kwargs) # Create default mechanisms for Gene Expression @@ -49,22 +175,50 @@ def __init__(self, name='', **kwargs): mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) # Create global mechanism for dilution dilution_mechanism = Dilution( name='dilution', filter_dict={'dna': False}, default_on=True ) global_mechanisms = {'dilution': dilution_mechanism} - self.add_mechanisms(global_mechanisms) + self.add_mechanisms(global_mechanisms, overwrite=None) + + def compile_crn(self, **kwargs) -> ChemicalReactionNetwork: + """Compile CRN with transcript generation disabled in gene expression. + + Overrides the parent `compile_crn` method to automatically disable + transcript generation in DNAassemblies that produce proteins. This + ensures that gene expression proceeds directly from DNA to protein + without intermediate mRNA species. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the parent Mixture + `compile_crn` method. + + Returns + ------- + ChemicalReactionNetwork + Compiled chemical reaction network with expression and dilution + reactions. + + Notes + ----- + This method automatically modifies DNAassemblies before compilation: - def compile_crn(self, **keywords) -> ChemicalReactionNetwork: - """Compile CRN, replacing transcripts with proteins. + - For assemblies with a protein product, sets transcript to False + - RNA-only assemblies (no protein) are not affected + - Mechanisms receive protein instead of transcript when transcript + is disabled - Overwriting compile_crn to turn off transcription in all - DNAassemblies. + This behavior enables the single-step expression mechanism to route + production directly to protein. - :return: compiled CRN instance + See `Mixture.compile_crn + ` for a more detailed + description of the parent method behavior. """ for component in self.components: @@ -78,26 +232,153 @@ def compile_crn(self, **keywords) -> ChemicalReactionNetwork: component.update_transcript(False) # Call the superclass function - return Mixture.compile_crn(self, **keywords) + return Mixture.compile_crn(self, **kwargs) class SimpleTxTlDilutionMixture(Mixture): - """Mixture with continuous dilution for non-DNA species. - - Transcription and Translation are both modeled as catalytic with no - cellular machinery. mRNA is also degraded via a separate reaction to - represent endonucleases. + """In vivo TX-TL with simple mechanisms and continuous dilution. + + A mixture that models transcription and translation as separate catalytic + reactions without explicitly representing cellular machinery (RNAP, + ribosomes). This mixture uses simple mass-action kinetics where DNA and + mRNA act as catalysts for transcript and protein production, + respectively. Includes global dilution to model cell growth and division + effects, plus separate RNA degradation to model endonuclease activity. + + This mixture is appropriate for in vivo gene expression models where + machinery is not limiting but explicit TX-TL steps and growth dilution + are important. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + ExpressionDilutionMixture : Single-step expression with dilution. + TxTlDilutionMixture : TX-TL with machinery and dilution. + SimpleTxTlExtract : TX-TL without dilution. + Mixture : Base class for all mixtures. + + Notes + ----- + Default mechanisms included: + + - 'transcription' : `SimpleTranscription` - Simple catalytic + transcription (DNA --> DNA + mRNA) without explicit RNAP binding + - 'translation' : `SimpleTranslation` - Simple catalytic translation + (mRNA --> mRNA + Protein) without explicit ribosome binding + - 'catalysis' : `BasicCatalysis` - Simple catalytic reactions without + explicit enzyme binding + - 'binding' : `One_Step_Binding` - Simple multi-species binding + - 'dilution' : `Dilution` - Global dilution mechanism (Species --> ∅) + applied to all non-DNA species to model growth/division + - 'rna_degradation' : `Dilution` - Separate RNA degradation mechanism + (mRNA --> ∅) applied to all RNA species to model endonuclease + activity + + Key features of this mixture: + + - Explicit transcription and translation steps + - Intermediate mRNA species + - Simple mass-action kinetics (no enzyme binding) + - No cellular machinery (RNAP, ribosomes) + - Global dilution of all non-DNA species + - Separate RNA degradation (faster than dilution) + - Models growth effects in vivo + + Common applications include: + + - In vivo gene circuit modeling with growth + - Models where machinery is not limiting + - Constitutive or weakly regulated promoters in growing cells + - mRNA dynamics with degradation and dilution + + Examples + -------- + Create a simple in vivo TX-TL mixture with dilution for GFP: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.SimpleTxTlDilutionMixture( + ... name='cell_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/cell_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ - def __init__(self, name='', **keywords): - """Initializes a SimpleTxTlDilutionMixture instance. - - :param name: name of the mixture - :param kwargs: keywords passed into the parent Class (Mixture) - """ - # Always call the superclass __init__ with **keywords - Mixture.__init__(self, name=name, **keywords) + def __init__(self, name='', **kwargs): + # Always call the superclass __init__ with **kwargs + Mixture.__init__(self, name=name, **kwargs) # Create TxTl Mechanisms # Transcription will not involve machinery @@ -112,7 +393,7 @@ def __init__(self, name='', **keywords): mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) # Global Dilution Mechanisms # By Default Species are diluted S-->0 Unless: @@ -131,31 +412,186 @@ def __init__(self, name='', **keywords): 'dilution': dilution_mechanism, 'rna_degradation': deg_mrna, } - self.add_mechanisms(global_mechanisms) + self.add_mechanisms(global_mechanisms, overwrite=None) class TxTlDilutionMixture(Mixture): - """Transcription and translation with expression machinery. - - This model includes a background load "cellular processes" which - represents innate loading effects in the cell. Effects of loading - on cell growth are not modelled. Unlike TxTlExtract, has global - dilution for non-DNA and non-Machinery This model does not include - any energy. + """In vivo TX-TL with explicit machinery, dilution, and background load. + + A mixture that models transcription and translation with explicit + representation of RNA polymerase (RNAP), ribosomes, and RNases for in + vivo contexts. This mixture uses Michaelis-Menten kinetics for TX-TL, + explicitly tracking enzyme-substrate binding and catalysis. Includes + global dilution to model cell growth effects and a background load + component representing endogenous cellular processes that compete for + shared machinery. + + Unlike `TxTlExtract`, this mixture includes dilution for non-DNA and + non-machinery species. Machinery components (RNAP, ribosomes, RNases) are + protected from dilution via the 'machinery' attribute. This model does + not include explicit energy species. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + rnap : str, default='RNAP' + Name for the RNA polymerase protein species. + ribosome : str, default='Ribo' + Name for the ribosome protein species. + rnaase : str, default='RNAase' + Name for the ribonuclease protein species. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + rnap : Protein + RNA polymerase component with 'machinery' attribute. + ribosome : Protein + Ribosome component with 'machinery' attribute. + rnaase : Protein + Ribonuclease component with 'machinery' attribute. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + SimpleTxTlDilutionMixture : TX-TL without machinery, with dilution. + TxTlExtract : TX-TL with machinery, without dilution. + ExpressionDilutionMixture : Single-step expression with dilution. + Mixture : Base class for all mixtures. + + Notes + ----- + This mixture automatically adds the following components: + + - RNA polymerase (RNAP) with 'machinery' attribute + - Ribosome with 'machinery' attribute + - Ribonuclease (RNase) with 'machinery' attribute + - Background processes DNAassembly representing cellular load + + Default mechanisms included: + + - 'transcription' : `Transcription_MM` - Michaelis-Menten transcription + with explicit RNAP binding (DNA + RNAP <--> DNA:RNAP --> DNA + RNAP + + mRNA) + - 'translation' : `Translation_MM` - Michaelis-Menten translation with + explicit ribosome binding (mRNA + Rib <--> mRNA:Rib --> mRNA + Rib + + Protein) + - 'rna_degradation' : `Degradation_mRNA_MM` - Global RNA degradation by + RNase using Michaelis-Menten kinetics + - 'catalysis' : `MichaelisMenten` - General Michaelis-Menten enzyme + catalysis + - 'binding' : `One_Step_Binding` - Simple multi-species binding + - 'dilution' : `Dilution` - Global dilution mechanism (Species --> ∅) + applied to all species except DNA and machinery + + Key features of this mixture: + + - Explicit modeling of transcription and translation machinery + - Resource competition (genes and background processes compete for + machinery) + - Enzyme sequestration in complexes + - RNA degradation dynamics + - Global dilution modeling cell growth + - Machinery protected from dilution + - Background load representing cellular processes + - Suitable for modeling in vivo gene expression with resource limits + + Background processes: + + - Implemented as a DNAassembly component ('cellular_processes') + - Represents endogenous genes competing for machinery + - Uses average promoter and RBS parameters + - Creates realistic loading effects on available machinery + - Does not model effects of loading on cell growth rate + + Common applications include: + + - In vivo gene circuit modeling with growth + - Resource allocation in growing cells + - Gene expression burden studies + - Competition between heterologous and endogenous genes + - Synthetic biology in cellular contexts + + Examples + -------- + Create an in vivo TX-TL mixture with machinery and dilution for GFP: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.TxTlDilutionMixture( + ... name='cell_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/cell_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ def __init__( self, name='', rnap='RNAP', ribosome='Ribo', rnaase='RNAase', **kwargs ): - """Initializes a TxTlDilutionMixture instance. - - :param name: name of the mixture - :param rnap: name of the RNA polymerase, default: RNAP - :param ribosome: name of the ribosome, default: Ribo - :param rnaase: name of the Ribonuclease, default: RNAase - :param kwargs: keywords passed into the parent Class (Mixture) - """ Mixture.__init__(self, name=name, **kwargs) # Create Components for TxTl machinery @@ -215,4 +651,4 @@ def __init__( 'dilution': dilution_mechanism, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) diff --git a/biocrnpyler/mixtures/cell_parameters.tsv b/biocrnpyler/mixtures/cell_parameters.tsv new file mode 100644 index 00000000..139bd141 --- /dev/null +++ b/biocrnpyler/mixtures/cell_parameters.tsv @@ -0,0 +1,21 @@ +mechanism part_id param_name value units comments +gene_expression kexpress 0.28125 The product of the above two rates + e coli Ribo 150 uM assuming ~100000 Ribosomes / e. coli with a volume 1 um^3 + e coli RNAP 15 uM assuming ~10000 RNAP molecules / e. coli with a volume 1 um^3 + e coli RNAase 45 uM assuming ~30000 RNAP molecules / e. coli with a volume 1 um^4 + e coli cellular_processes 5 somewhat arbitary concentration for ~3000 genes in e. coli assuming weak loading on all of them + e coli extract protein_Ribo 24 1/5 th the Ribosome concentration of E. Coli + e coli extract protein_RNAP 3 1/5 th the rnap concentration of E. Coli + e coli extract protein_RNAase 6 1/5 th the rnaase concentration of E. Coli + e coli extract 2 protein_Ribo 12 + e coli extract 2 protein_RNAP 6 + e coli extract 2 protein_RNAase 3 + ktx 0.05 transcripts / second per polymerase assuming 50nt/s and transcript length of 1000 + ktl 0.05 proteins / second per ribosome assuming 15aa/s and protein length of 300 + cooperativity 2 Seems like a good default + kb 100 assuming 10ms to diffuse across 1um (characteristic cell size) + ku 10 """90% binding""" + kdil 0.001 assuming half life of ~20 minutes for everything (e coli doubling time) +rna_degradation_mm kdeg 1.01 The values from Singhal et al Supplemental Table S2 +rna_degradation_mm kb 1 The values from Singhal et al Supplemental Table S2 +rna_degradation_mm ku 1.26582E-06 The values from Singhal et al Supplemental Table S2 diff --git a/biocrnpyler/mixtures/extract.py b/biocrnpyler/mixtures/extract.py index 31df2d40..4a54d7a4 100644 --- a/biocrnpyler/mixtures/extract.py +++ b/biocrnpyler/mixtures/extract.py @@ -22,19 +22,142 @@ class ExpressionExtract(Mixture): - """Gene expression without any machinery (ribosomes, polymerases, etc.). - - Here transcription and Translation are lumped into one reaction: - expression. + """Gene expression extract without explicit TX-TL machinery. + + A simplified mixture that models gene expression as a single direct + reaction from DNA to protein, without explicitly representing + transcription and translation as separate processes. This extract lumps + transcription and translation into a single 'expression' reaction, + eliminating intermediate mRNA species and cellular machinery (ribosomes, + polymerases, etc.). + + This extract is appropriate for coarse-grained models where mRNA dynamics + are negligible and computational efficiency is prioritized over + mechanistic detail. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + SimpleTxTlExtract : TX-TL with separate transcription and translation. + TxTlExtract : TX-TL with explicit machinery. + OneStepGeneExpression : Mechanism used for expression. + Mixture : Base class for all mixtures. + + Notes + ----- + Default mechanisms included: + + - 'transcription' : `OneStepGeneExpression` - Single-step gene + expression (DNA --> DNA + Protein) without intermediate mRNA + - 'translation' : `EmptyMechanism` - Dummy mechanism that generates no + reactions (translation is disabled) + - 'catalysis' : `BasicCatalysis` - Simple catalytic reactions without + explicit enzyme binding + - 'binding' : `One_Step_Binding` - Simple multi-species binding + + Key features of this extract: + + - No explicit transcription or translation steps + - No cellular machinery (RNAP, ribosomes, RNases) + - No intermediate mRNA species + - Simplified parameter space (single 'kexpress' rate) + - Fast compilation and simulation + + When compiled, this extract automatically disables transcript generation + in DNA assemblies that produce proteins, routing expression directly from + DNA to protein. + + Common applications include: + + - High-level gene circuit modeling + - Steady-state or quasi-steady-state analyses + - Rapid prototyping of genetic designs + - Models where mRNA dynamics are negligible + + Examples + -------- + Create an expression mixture for GFP production: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... protein='GFP' + ... ) + >>> mixture = bcp.ExpressionExtract( + ... name='expression_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/extract_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ def __init__(self, name='', **kwargs): - """Initializes an ExpressionExtract instance. - - :param name: name of the mixture - :param kwargs: keywords passed into the parent Class (Mixture) - """ # always call the superlcass Mixture.__init__(...) Mixture.__init__(self, name=name, **kwargs) @@ -53,15 +176,43 @@ def __init__(self, name='', **kwargs): mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) + + def compile_crn(self, **kwargs) -> ChemicalReactionNetwork: + """Compile CRN with transcript generation disabled in gene expression. + + Overrides the parent `compile_crn` method to automatically disable + transcript generation in DNA assemblies that produce proteins. This + ensures that gene expression proceeds directly from DNA to protein + without intermediate mRNA species. - def compile_crn(self, **keywords) -> ChemicalReactionNetwork: - """Compile CRN, turning off transcription. + Parameters + ---------- + **kwargs + Additional keyword arguments passed to the parent Mixture + `compile_crn ` + method. - Overwriting compile_crn to turn off transcription in all - DNAassemblies. + Returns + ------- + ChemicalReactionNetwork + Compiled chemical reaction network with expression reactions. - :return: compiled CRN instance + Notes + ----- + This method automatically modifies DNA assemblies before compilation: + + - For assemblies with a protein product, sets transcript to False + - RNA-only assemblies (no protein) are not affected + - Mechanisms receive protein instead of transcript when transcript + is disabled + + This behavior enables the single-step expression mechanism to route + production directly to protein. + + See `Mixture.compile_crn + ` for a more detailed + description of the parent method behavior. """ for component in self.components: @@ -77,23 +228,147 @@ def compile_crn(self, **keywords) -> ChemicalReactionNetwork: component.update_transcript(False) # Call the superclass function - return Mixture.compile_crn(self, **keywords) + return Mixture.compile_crn(self, **kwargs) class SimpleTxTlExtract(Mixture): - """Transcription and translation in extract w/out any machinery. - - Transcriptoin and translation without ribosomes, polymerases, - etc. RNA is degraded via a global mechanism. + """TX-TL extract with simple transcription and translation mechanisms. + + A mixture that models transcription and translation as separate catalytic + reactions without explicitly representing cellular machinery (RNAP, + ribosomes, RNases). This extract uses simple mass-action kinetics where + DNA and mRNA act as catalysts for transcript and protein production, + respectively. Unlike `ExpressionExtract`, this mixture includes explicit + mRNA species and separate TX-TL steps. Unlike `TxTlExtract`, it does not + model enzyme binding or resource competition. + + This extract includes global RNA degradation via dilution. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + ExpressionExtract : Single-step expression without transcripts. + TxTlExtract : TX-TL with explicit machinery. + SimpleTranscription : Mechanism used for transcription. + SimpleTranslation : Mechanism used for translation. + Mixture : Base class for all mixtures. + + Notes + ----- + Default mechanisms included: + + - 'transcription' : `SimpleTranscription` - Simple catalytic + transcription (DNA --> DNA + mRNA) without explicit RNAP binding + - 'translation' : `SimpleTranslation` - Simple catalytic + translation (mRNA --> mRNA + Protein) without explicit ribosome binding + - 'rna_degradation' : `Dilution` - Global RNA degradation mechanism + (mRNA --> ∅) applied to all RNA species + - 'catalysis' : `BasicCatalysis` - Simple catalytic reactions without + explicit enzyme binding + - 'binding' : `One_Step_Binding` - Simple multi-species binding + + Key features of this extract: + + - Explicit transcription and translation steps + - Intermediate mRNA species + - Simple mass-action kinetics (no enzyme binding) + - No cellular machinery (RNAP, ribosomes) + - Global RNA degradation + - Faster simulation than Michaelis-Menten models + + Common applications include: + + - Gene circuit modeling with explicit TX-TL + - Models where machinery is not limiting + - Constitutive or weakly regulated promoters + - Rapid prototyping with mRNA dynamics + + Examples + -------- + Create a simple TX-TL mixture for GFP expression: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.SimpleTxTlExtract( + ... name='simple_txtl_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/extract_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ def __init__(self, name='', **kwargs): - """Initializes a SimpleTxTlExtract instance. - - :param name: name of the mixture - :param kwargs: keywords passed into the parent Class (Mixture) - """ # Always call the superlcass Mixture.__init__(...) Mixture.__init__(self, name=name, **kwargs) @@ -109,7 +384,7 @@ def __init__(self, name='', **kwargs): mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=False) # global mechanisms for dilution and rna degradation mech_rna_deg_global = Dilution( @@ -118,31 +393,167 @@ def __init__(self, name='', **kwargs): default_on=False, ) global_mechanisms = {'rna_degradation': mech_rna_deg_global} - self.add_mechanisms(global_mechanisms) + self.add_mechanisms(global_mechanisms, overwrite=None) class TxTlExtract(Mixture): - """Transcription and translation with expression machinery. - - A Model for Transcription and Translation in Cell Extract with - Ribosomes, Polymerases, and Endonucleases. - - This model does not include any energy. + """TX-TL extract with explicit transcription and translation machinery. + + A mixture that models transcription and translation with explicit + representation of RNA polymerase (RNAP), ribosomes, and RNases. This + extract uses Michaelis-Menten kinetics for transcription and translation, + explicitly tracking enzyme-substrate binding and catalysis. Unlike + `SimpleTxTlExtract`, this mixture models resource competition and enzyme + sequestration effects. + + This model does not include explicit energy species. For energy-aware + modeling, use `EnergyTxTlExtract`. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + rnap : str, default='RNAP' + Name for the RNA polymerase protein species. + ribosome : str, default='Ribo' + Name for the ribosome protein species. + rnaase : str, default='RNAase' + Name for the ribonuclease protein species. + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + rnap : Protein + RNA polymerase component. + ribosome : Protein + Ribosome component. + rnaase : Protein + Ribonuclease component. + + See Also + -------- + SimpleTxTlExtract : TX-TL without explicit machinery. + EnergyTxTlExtract : TX-TL with explicit energy consumption. + ExpressionExtract : Combined TX-TL without transcripts. + Mixture : Base class for all mixtures. + + Notes + ----- + This mixture automatically adds the following components: + + - RNA polymerase (RNAP) + - Ribosome + - Ribonuclease (RNase) + + Default mechanisms included: + + - 'transcription' : `Transcription_MM` - Michaelis-Menten transcription + with explicit RNAP binding (DNA + RNAP <--> DNA:RNAP --> DNA + RNAP + + mRNA) + - 'translation' : `Translation_MM` - Michaelis-Menten translation with + explicit ribosome binding (mRNA + Rib <--> mRNA:Rib --> mRNA + Rib + + Protein) + - 'rna_degradation' : `Degradation_mRNA_MM` - Global RNA degradation by + RNase using Michaelis-Menten kinetics + - 'catalysis' : `MichaelisMenten` - General Michaelis-Menten enzyme + catalysis + - 'binding' : `One_Step_Binding` - Simple multi-species binding + + Key features of this mixture: + + - Explicit modeling of transcription and translation machinery + - Resource competition effects (multiple genes compete for RNAP) + - Enzyme sequestration in complexes + - RNA degradation dynamics + - Suitable for modeling TX-TL systems with limited machinery + + Common applications include: + + - Cell-free TX-TL systems + - Resource allocation in gene circuits + - Gene expression burden studies + - Synthetic biology prototyping + + Examples + -------- + Create a TX-TL mixture for GFP expression: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.TxTlExtract( + ... name='txtl_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/extract_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ def __init__( self, name='', rnap='RNAP', ribosome='Ribo', rnaase='RNAase', **kwargs ): - """Initializes a TxTlExtract instance. - - :param name: name of the mixture - :param rnap: name of the RNA polymerase, default: RNAP - :param ribosome: name of the ribosome, default: Ribo - :param rnaase: name of the Ribonuclease, default: RNAase - :param kwargs: keywords passed into the parent Class (Mixture) - - """ # Always call the superlcass Mixture.__init__(...) Mixture.__init__(self, name=name, **kwargs) @@ -168,19 +579,195 @@ def __init__( mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) class EnergyTxTlExtract(Mixture): - """Transcription and translation in extract with machinery, energy. - - This model include energy carrier molcules in the form of NTPs, - Amino Acids, and a Fuel Species (such as 3PGA) used for NTP - regeneration. This model is equivalent to TxTl extract, but with - limited fuel. Note that different amino acids and nucleotides are - lumped together. - - Energy usage for transcription and translation is length dependent. + """TX-TL cell extract with explicit machinery and energy consumption. + + A mixture that models transcription and translation with explicit + representation of RNA polymerase (RNAP), ribosomes, RNases, and energy + carrier molecules. This extract uses Michaelis-Menten kinetics with + length-dependent fuel consumption to model realistic TX-TL energetics. + Unlike `TxTlExtract`, this mixture explicitly tracks NTPs, amino acids, + and fuel species (e.g., 3PGA for NTP regeneration). + + Energy usage for transcription and translation is length-dependent, + reflecting the stoichiometric consumption of NTPs and amino acids during + biopolymer synthesis. + + Parameters + ---------- + name : str, default='' + Name of the mixture for identification and parameter lookup. + rnap : str, default='RNAP' + Name for the RNA polymerase protein species. + ribosome : str, default='Ribo' + Name for the ribosome protein species. + rnaase : str, default='RNAase' + Name for the ribonuclease protein species. + ntps : str, default='NTPs' + Name for the nucleotide triphosphate species (lumped NTPs). + ndps : str, default='NDPs' + Name for the nucleotide diphosphate species (lumped NDPs). + amino_acids : str, default='amino_acids' + Name for the amino acid species (lumped amino acids). + fuel : str, default='Fuel_3PGA' + Name for the fuel species used for NTP regeneration (e.g., 3PGA). + mechanisms : dict, list, or Mechanism, optional + Default mechanisms for components in this mixture. Can be a dict with + mechanism types (str) as keys and mechanism objects as values, a + list of mechanism objects, or a single `Mechanism`. + components : list of Component or Component, optional + Components to include in the mixture. Components are deep-copied when + added to prevent modification of original objects. + parameters : dict, optional + Dictionary of parameter values. Keys follow the format + (mechanism, part_id, param_name). + compartment : Compartment, optional + Default compartment for all components and species in this mixture. + parameter_file : str, optional + Path to a CSV or TSV file containing parameters to load. + overwrite_parameters : bool, default=False + If True, parameters from file/dict overwrite existing parameters. + If False, existing parameters are preserved. + global_mechanisms : dict, list, or GlobalMechanism, optional + Global mechanisms that apply to all species after component + compilation (e.g., dilution, global degradation). Can be a dict, + list, or single `GlobalMechanism`. + species : list of Species or Species, optional + Additional species to add directly to the CRN without going through + component compilation. + initial_condition_dictionary : dict, optional + Dictionary mapping species to initial concentration values. Deprecated + in favor of using parameters with mechanism='initial concentration'. + global_component_enumerators : list, optional + List of global component enumerators for advanced component generation + patterns (e.g., creating all pairwise interactions). + global_recursion_depth : int, default=4 + Maximum recursion depth for global component enumeration during + compilation. + local_recursion_depth : int, optional + Maximum recursion depth for local component enumeration. If None, + defaults to `global_recursion_depth + 2`. + + Attributes + ---------- + name : str + Name of the mixture. + rnap : Protein + RNA polymerase component. + ribosome : Protein + Ribosome component. + rnaase : Protein + Ribonuclease component. + amino_acids : Metabolite + Amino acid metabolite component. + fuel : Metabolite + Fuel metabolite component for ATP regeneration. + ndps : Metabolite + Nucleotide diphosphate metabolite component. + ntps : Metabolite + Nucleotide triphosphate metabolite component with fuel-dependent + regeneration. + compartment : Compartment or None + Default compartment for the mixture. + components : list of Component + List of components in the mixture (deep copies of added components). + mechanisms : dict + Dictionary of default mechanisms, keyed by mechanism type (str). + global_mechanisms : dict + Dictionary of global mechanisms, keyed by mechanism type (str). + parameter_database : ParameterDatabase + Database storing all parameters for this mixture. + added_species : list of Species + List of species added directly to the mixture. + global_component_enumerators : list + List of global component enumerators. + global_recursion_depth : int + Recursion depth for global component enumeration. + local_recursion_depth : int + Recursion depth for local component enumeration. + crn : ChemicalReactionNetwork or None + The compiled CRN, created by calling `compile_crn`. + + See Also + -------- + TxTlExtract : TX-TL with machinery but no energy. + SimpleTxTlExtract : TX-TL without machinery or energy. + Energy_Transcription_MM : Mechanism for energy-consuming transcription. + Energy_Translation_MM : Mechanism for energy-consuming translation. + Mixture : Base class for all mixtures. + + Notes + ----- + This mixture automatically adds the following components: + + - RNA polymerase (RNAP) + - Ribosome + - Ribonuclease (RNase) + - Amino acids (lumped) + - NTPs (nucleotide triphosphates, lumped) + - NDPs (nucleotide diphosphates, lumped) + - Fuel (e.g., 3PGA for ATP regeneration) + + Default mechanisms included: + + - 'transcription' : `Energy_Transcription_MM` - Michaelis-Menten + transcription with length-dependent NTP consumption (DNA + RNAP <--> + DNA:RNAP; NTP + DNA:RNAP --> DNA + RNAP + mRNA + NDP) + - 'translation' : `Energy_Translation_MM` - Michaelis-Menten translation + with length-dependent amino acid and NTP consumption (mRNA + Rib <--> + mRNA:Rib; AA + NTP + mRNA:Rib --> mRNA + Rib + Protein + NDP) + - 'rna_degradation' : `Degradation_mRNA_MM` - Global RNA degradation by + RNase using Michaelis-Menten kinetics + - 'catalysis' : `MichaelisMenten` - General Michaelis-Menten enzyme + catalysis + - 'binding' : `One_Step_Binding` - Simple multi-species binding + - 'pathway' : `OneStepPathway` - Metabolite conversion (added to NTPs + and fuel components) + + Key features of this mixture: + + - Explicit modeling of transcription and translation machinery + - Length-dependent energy consumption + - NTP regeneration from fuel species + - Resource competition and depletion effects + - Realistic modeling of TX-TL resource limits + - Energy-dependent expression dynamics + + Energy model details: + + - Transcription consumes L NTPs per mRNA of length L + - Translation consumes L amino acids and 4L NTPs per protein of length L + - Fuel species regenerates NTPs from NDPs + - Different nucleotides and amino acids are lumped together + + Common applications include: + + - Cell-free TX-TL systems with limited resources + - Models of energy-limited gene expression + - Resource allocation and burden studies + - TX-TL system optimization + - Metabolic coupling with gene expression + + Examples + -------- + Create an energy-aware TX-TL mixture for GFP expression: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.EnergyTxTlExtract( + ... name='energy_txtl_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/extract_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ @@ -196,20 +783,6 @@ def __init__( fuel='Fuel_3PGA', **kwargs, ): - """Initailize the TX-TL mixture. - - :param name: name of the mixture - :param rnap: name of the RNA polymerase, default: RNAP - :param ribosome: name of the ribosome, default: Ribo - :param rnaase: name of the Ribonuclease, default: RNAase - :param ntps: name of the nucleotide fuel source (eg ATP + GTP etc), - default: NTP - :param amino_acids: name of the amino acids species, default: - amino_acids - :param fuel: name of the fuel species that regenerates ATP - :param kwargs: keywords passed into the parent Class (Mixture) - - """ Mixture.__init__(self, name=name, **kwargs) # create default Components to represent cellular machinery @@ -227,8 +800,8 @@ def __init__( # These mechanisms are Component specific and only added to # the NTPs metabolite mech_pathway = OneStepPathway() - self.ntps.add_mechanisms(mech_pathway) - self.fuel.add_mechanisms(mech_pathway) + self.ntps.add_mechanisms(mech_pathway, overwrite=None) + self.fuel.add_mechanisms(mech_pathway, overwrite=None) default_components = [ self.rnap, @@ -263,4 +836,4 @@ def __init__( mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) diff --git a/biocrnpyler/mixtures/extract_parameters.tsv b/biocrnpyler/mixtures/extract_parameters.tsv index 28cc4b07..9609316c 100644 --- a/biocrnpyler/mixtures/extract_parameters.tsv +++ b/biocrnpyler/mixtures/extract_parameters.tsv @@ -1,20 +1,26 @@ -mechanism part_id param_name value units comments -energy_transcription_mm ktx 3.25 Tx_cat from Singhal et al. -energy_transcription_mm kb 4.48 For the promoter tested in Singhal et al Supplemental Table S2 -energy_transcription_mm ku 2.48889E-06 For the promoter tested in Singhal et al Supplemental Table S2 -energy_transcription_mm length 300 "This is a default length, gene specific ones should be set" -energy_translation_mm ktl 19.2 TL_cat from Singhal et al. -energy_translation_mm kb 0.819 For the RBS tested in Singhal et al Supplemental Table S2 -energy_translation_mm ku 0.002853659 For the RBS tested in Singhal et al Supplemental Table S2 -energy_translation_mm length 100 "This is a default length, transcript specific ones should be set" -rna_degradation_mm kdeg 1.01 The values from Singhal et al Supplemental Table S2 -rna_degradation_mm kb 1 The values from Singhal et al Supplemental Table S2 -rna_degradation_mm ku 1.26582E-06 The values from Singhal et al Supplemental Table S2 -one_step_pathway NTPs_production k 0.02 alpha_atp fron Singhal et al. -one_step_pathway NTPs_degradation k 0.0000177 Delta_ATP from Singhal et a. -initial concentration NTPs 5 mM (total NTPs in standard energy buffer) -initial concentration amino_acids 30 mM mM (total amino acids in standard energy buffer) -initial concentration Fuel_3PGA 30 mM mM (standard energy buffer amount) -initial concentration RNAase 20.2 The values from Singhal et al Supplemental Table S2 -initial concentration protein_Ribo 0.0273 The values from Singhal et al Supplemental Table S2 -initial concentration RNAP 0.00933 The values from Singhal et al Supplemental Table S2 +mechanism part_id param_name value units comments +gene_expression kexpress 0.28125 The product of the above two rates + ktx 0.05 transcripts / second per polymerase assuming 50nt/s and transcript length of 1000 + ktl 0.05 proteins / second per ribosome assuming 15aa/s and protein length of 300 + kb 100 assuming 10ms to diffuse across 1um (characteristic cell size) + ku 10 90% binding + kdil 0.001 assuming half life of ~20 minutes for everything (e coli doubling time) +energy_transcription_mm ktx 3.25 Tx_cat from Singhal et al. +energy_transcription_mm kb 4.48 For the promoter tested in Singhal et al Supplemental Table S2 +energy_transcription_mm ku 2.48889E-06 For the promoter tested in Singhal et al Supplemental Table S2 +energy_transcription_mm length 300 "This is a default length, gene specific ones should be set" +energy_translation_mm ktl 19.2 TL_cat from Singhal et al. +energy_translation_mm kb 0.819 For the RBS tested in Singhal et al Supplemental Table S2 +energy_translation_mm ku 0.002853659 For the RBS tested in Singhal et al Supplemental Table S2 +energy_translation_mm length 100 "This is a default length, transcript specific ones should be set" +rna_degradation_mm kdeg 1.01 The values from Singhal et al Supplemental Table S2 +rna_degradation_mm kb 1 The values from Singhal et al Supplemental Table S2 +rna_degradation_mm ku 1.26582E-06 The values from Singhal et al Supplemental Table S2 +one_step_pathway NTPs_production k 0.02 alpha_atp fron Singhal et al. +one_step_pathway NTPs_degradation k 0.0000177 Delta_ATP from Singhal et a. +initial concentration NTPs 5 mM (total NTPs in standard energy buffer) +initial concentration amino_acids 30 mM mM (total amino acids in standard energy buffer) +initial concentration Fuel_3PGA 30 mM mM (standard energy buffer amount) +initial concentration RNAase 20.2 The values from Singhal et al Supplemental Table S2 +initial concentration protein_Ribo 0.0273 The values from Singhal et al Supplemental Table S2 +initial concentration RNAP 0.00933 The values from Singhal et al Supplemental Table S2 diff --git a/biocrnpyler/mixtures/pure.py b/biocrnpyler/mixtures/pure.py index 3d57924b..fc666e25 100644 --- a/biocrnpyler/mixtures/pure.py +++ b/biocrnpyler/mixtures/pure.py @@ -10,18 +10,166 @@ class BasicPURE(Mixture): - """Reconstituted protein synthesis system with resource limits. + """PURE cell-free protein synthesis system with energy consumption. - This model includes energy carrier molecules in the form of NTPs, amino - acids, and a fuel species (such as ATP) used for transcription, - translation, and other core mechanisms. This model is equivalent to - `EnergyTxTlExtract`, but without a fuel generation mechanism. Amino - acids and nucleotides are lumped together into a single meta-species. + A mixture that models the PURE (Protein synthesis Using Recombinant + Elements) reconstituted cell-free transcription-translation system with + explicit representation of RNA polymerase (RNAP), ribosomes, RNases, and + energy carrier molecules. This extract uses Michaelis-Menten kinetics + with length-dependent fuel consumption to model realistic TX-TL + energetics. - Note that fuel is modeled as a separate molecule so if the default - 'ATP' is used, it is separate from the other nucleotides ('NTPs'). + Unlike `EnergyTxTlExtract`, this mixture does not include fuel + regeneration mechanisms. Energy carriers (ATP, NTPs, amino acids) are + consumed but not regenerated, making this suitable for modeling + resource-limited PURE systems. Different amino acids and nucleotides are + lumped into single meta-species for simplicity. - Energy usage for transcription and translation is length dependent. + Note that fuel (default 'ATP') is modeled as a separate molecule from + other nucleotides ('NTPs'), allowing independent tracking of energy + consumption. + + Energy usage for transcription and translation is length-dependent, + reflecting stoichiometric consumption during biopolymer synthesis. + + Parameters + ---------- + name : str, default='PURE' + Name identifier for the mixture. + rnap : str, default='RNAP' + Name for the RNA polymerase protein species. + ribosome : str, default='Ribo' + Name for the ribosome protein species. + rnaase : str, default='RNAase' + Name for the ribonuclease protein species. + ntps : str, default='NTPs' + Name for the nucleotide triphosphate species (lumped NTPs excluding + ATP). + ndps : str, default='NDPs' + Name for the nucleotide diphosphate species (lumped NDPs). + amino_acids : str, default='AAs' + Name for the amino acid species (lumped amino acids). + fuel : str, default='ATP' + Name for the primary energy carrier species (ATP). + parameter_file : str, default='mixtures/pure_parameters.tsv' + Path to file containing default parameter values for the PURE + system. + **kwargs + Additional keyword arguments passed to the parent Mixture class. + + Attributes + ---------- + rnap : Protein + RNA polymerase component. + ribosome : Protein + Ribosome component. + rnaase : Protein + Ribonuclease component. + ntps : Metabolite + Nucleotide triphosphate metabolite component (excluding ATP). + amino_acids : Metabolite + Amino acid metabolite component. + fuel : Metabolite + Fuel metabolite component (ATP). + name : str + Name of the mixture. + + See Also + -------- + EnergyTxTlExtract : TX-TL with fuel regeneration. + TxTlExtract : TX-TL with machinery but no energy. + Energy_Transcription_MM : Mechanism for energy-consuming transcription. + Energy_Translation_MM : Mechanism for energy-consuming translation. + Mixture : Base class for all mixtures. + + Notes + ----- + This mixture automatically adds the following components: + + - RNA polymerase (RNAP) + - Ribosome + - Ribonuclease (RNase) + - Amino acids (lumped) + - NTPs (nucleotide triphosphates excluding ATP, lumped) + - NDPs (nucleotide diphosphates, lumped) + - Fuel (ATP for energy) + + Default mechanisms included: + + - 'transcription' : `Energy_Transcription_MM` - Michaelis-Menten + transcription with length-dependent ATP and NTP consumption + - 'translation' : `Energy_Translation_MM` - Michaelis-Menten translation + with length-dependent amino acid and ATP consumption + - 'rna_degradation' : `Degradation_mRNA_MM` - Global RNA degradation by + RNase using Michaelis-Menten kinetics + - 'catalysis' : `MichaelisMenten` - General Michaelis-Menten enzyme + catalysis for user-defined enzymatic reactions + - 'binding' : `One_Step_Binding` - Simple multi-species binding for + forming complexes + + Key features of this mixture: + + - Explicit modeling of PURE system components + - Length-dependent energy consumption (realistic stoichiometry) + - No fuel regeneration mechanisms (finite resource pool) + - Resource competition effects (genes compete for RNAP and ribosomes) + - Resource depletion dynamics (ATP, NTPs, amino acids deplete) + - Enzyme sequestration in complexes + - RNA degradation by RNase + - Separate tracking of ATP vs other NTPs + - Suitable for modeling batch-mode PURE reactions + + Energy model details: + + - Transcription: Consumes L NTPs and L ATPs per mRNA of length L + - Translation: Consumes L amino acids and 4L ATPs per protein of length + L (4 ATPs per amino acid reflect GTP hydrolysis during elongation) + - No regeneration: ATP, NTPs, and amino acids are consumed but not + regenerated + - Energy depletion: Expression stops when resources are exhausted + - Length parameter L: Represents gene/protein length in appropriate + units + - Lumped species: Different nucleotides lumped into NTPs, different + amino acids lumped into single species + - Separate ATP: ATP tracked separately from other NTPs for independent + energy accounting + + Differences from `EnergyTxTlExtract`: + + - No fuel regeneration pathway (no NTP regeneration from 3PGA or other + fuel sources) + - ATP modeled as separate fuel species rather than included in NTPs + - Default parameter file points to PURE-specific parameters + - Intended for modeling finite-resource batch reactions + - More realistic for in vitro PURE systems + + Common applications include: + + - PURE cell-free TX-TL systems + - Resource-limited gene expression modeling + - TX-TL system optimization with fixed resource budgets + - Batch mode TX-TL reactions + - Energy budget and resource allocation studies + - Multi-gene expression burden analysis + - In vitro synthetic biology applications + + Examples + -------- + Create a PURE mixture for GFP expression: + + >>> gfp_gene = bcp.DNAassembly( + ... name='gfp_construct', + ... promoter='pconst', + ... rbs='bcd2', + ... transcript='gfp_mrna', + ... protein='GFP' + ... ) + >>> mixture = bcp.BasicPURE( + ... name='pure_mixture', + ... components=[gfp_gene], + ... parameter_file='mixtures/pure_parameters.tsv' + ... ) + >>> crn = mixture.compile_crn() """ @@ -38,22 +186,6 @@ def __init__( parameter_file='mixtures/pure_parameters.tsv', **kwargs, ): - """Initialize the PURE mixture. - - :param name: name of the mixture - :param rnap: name of the RNA polymerase, default: RNAP - :param ribosome: name of the ribosome, default: Ribo - :param rnaase: name of the Ribonuclease, default: RNAase - :param ntps: name of the nucleotide fuel source (eg ATP + GTP etc), - default: NTP - :param amino_acids: name of the amino acids species, default: - amino_acids - :param fuel: name of the energy carrier species - :param parameter_file: file containing default parameter values - :param parameter: dictionary with parameter values - :param kwargs: keywords passed into the parent Class (Mixture) - - """ Mixture.__init__( self, name=name, parameter_file=parameter_file, **kwargs ) @@ -102,4 +234,4 @@ def __init__( mech_cat.mechanism_type: mech_cat, mech_bind.mechanism_type: mech_bind, } - self.add_mechanisms(default_mechanisms) + self.add_mechanisms(default_mechanisms, overwrite=None) diff --git a/biocrnpyler/utils/fileutil.py b/biocrnpyler/utils/fileutil.py index 0530495d..d873c1c9 100644 --- a/biocrnpyler/utils/fileutil.py +++ b/biocrnpyler/utils/fileutil.py @@ -1,5 +1,7 @@ -# findfile.py - find biocrnypyler file +# fileutil.py - file utilities for biocrnpyler # RMM (via Claude), 19 Sep 2025 +# +# Utilities for finding and managing files in the biocrnpyler package. import os from pathlib import Path @@ -11,28 +13,45 @@ def find_file_in_bcp_path( ) -> Optional[str]: """Find a file by searching through biocrnpyler paths. - Search order: - 1. Current directory - 2. Directories specified in environment variable (default: BCP_PATH) - 3. biocrnpyler package's 'defaults' subdirectory - - Args: - filename (str): Name of file to find - env_var_name (str): Environment variable containing search paths - - Returns: - str: Full path to file if found, None if not found - - Example: - >>> # Set environment variable (in shell or programmatically) - >>> os.environ['BCP_PATH'] = '/path/to/models:/path/to/configs' - >>> - >>> # Find a file - >>> filepath = find_file_in_bcp_path('model.xml') - >>> if filepath: - >>> print(f"Found file at: {filepath}") - >>> else: - >>> print("File not found") + Searches for a file in multiple locations in the following order: + + 1. Current working directory + 2. Directories specified in environment variable (default: 'BCP_PATH') + 3. biocrnpyler package directory + + The environment variable should contain a colon-separated (Unix) or + semicolon-separated (Windows) list of directory paths. + + Parameters + ---------- + filename : str + Name of file to find. + env_var_name : str, default='BCP_PATH' + Name of environment variable containing search paths. + + Returns + ------- + str or None + Full path to file if found, None otherwise. + + Examples + -------- + Set environment variable and find a file: + + >>> import os + >>> os.environ['BCP_PATH'] = '/path/to/models:/path/to/configs' + >>> filepath = find_file_in_bcp_path('model.xml') + >>> if filepath: + ... print(f'Found file at: {filepath}') + ... else: + ... print('File not found') + Found file at: /path/to/models/model.xml + + Search with a custom environment variable: + + >>> filepath = find_file_in_bcp_path( + ... 'parameters.csv', env_var_name='MY_PARAMS_PATH') + """ search_paths = [] diff --git a/biocrnpyler/utils/general.py b/biocrnpyler/utils/general.py index ee364fb5..9b8eda4a 100644 --- a/biocrnpyler/utils/general.py +++ b/biocrnpyler/utils/general.py @@ -34,7 +34,7 @@ def recursive_parent(s): def remove_bindloc(spec_list): - """Go through every species on a list and remove any "bindloc" attributes. + """Go through every species on a list and remove any 'bindloc' attributes. This is used to convert monomers with a parent polymer into the correct species after combinatorial binding in things like @@ -82,6 +82,7 @@ def combine_dictionaries(dict1, dict2): """Append lists that share the same key, and add new keys. WARNING: this only works if the dictionaries have values that are lists. + """ outdict = dict1 for key in dict2: @@ -97,7 +98,10 @@ def combine_dictionaries(dict1, dict2): def member_dictionary_search(member, dictionary): """Searches dictionary for keys relevant to the given data member. + Notes + ----- Order of returning: + repr name material_type diff --git a/biocrnpyler/utils/plotting.py b/biocrnpyler/utils/plotting.py index 6d12a703..64d5f4ac 100644 --- a/biocrnpyler/utils/plotting.py +++ b/biocrnpyler/utils/plotting.py @@ -90,7 +90,7 @@ def makeArrows2( headangle=math.pi / 6, make_arrows=True, ): - """This function draws an arrow shape at the end of graph lines.""" + """Draw an arrow shape at the end of graph lines.""" xs, ys = [], [] xbounds = [0, 0] ybounds = [0, 0] @@ -158,25 +158,32 @@ def graphPlot( rseed=30, show_species_images=False, ): - """Given a directed graph, plot it! - - Inputs: - DG: a directed graph of type DiGraph - DGspecies: a directed graph which only contains the species nodes - DGreactions: a directed graph which only contains the reaction nodes - plot: a bokeh plot object - layout: graph layout function. - 'force' uses fa2 to push nodes apart + """Given a directed graph, plot it. + + Parameters + ---------- + DG : DiGraph + A directed graph of type DiGraph. + DGspecies : DiGraph + A directed graph which only contains the species nodes. + DGreactions : DiGraph + A directed graph which only contains the reaction nodes. + plot : bokeh plot object + A bokeh plot object. + layout : str + Graph layout function. 'force' uses fa2 to push nodes apart. 'circle' plots the nodes and reactions in two overlapping - circles, with the reactions on the inside of the circle - 'custom' allows user input "layoutfunc". Internally, layoutfunc - is passed the three inputs (DG, DGspecies, DGreactions) and - should output a position dictionary with node - {:(x,y)} - positions: a dictionary of node names and x,y positions. this gets - passed into the layout function - posscale: multiply the scaling of the plot. This only affects the arrows - because the arrows are a hack :(. + circles, with the reactions on the inside of the circle. + 'custom' allows user input 'layoutfunc'. Internally, + 'layoutfunc' is passed the three inputs (DG, DGspecies, + DGreactions) and should output a position dictionary with node + {:(x,y)}. + positions : dict + A dictionary of node names and x,y positions. This gets passed + into the layout function. + posscale : float + Multiply the scaling of the plot. This only affects the arrows + because the arrows are a hack :(. """ random.seed(rseed) @@ -361,19 +368,25 @@ def generate_networkx_graph( ): """Generates a networkx DiGraph object that represents the CRN. - input: - ========================== - CRN: a CRN from mixture.get_model() for example - useweights: this will attempt to represent the reaction rates by the - length of edges. short edges are fast rates. It doesn't look - very good usually. - use_pretty_print: this uses the "pretty print" function to represent - reactions and nodes a bit cleaner - pp_show_material: default false because this is listed in "type" - pp_show_rates: default true because this is useful information - pp_show_attributes - colordict: a dictionary containing which node types are what color - based upon the following keywords: + Parameters + ---------- + CRN : ChemicalReactionNetwork + A CRN from mixture.get_model() for example. + useweights : bool + This will attempt to represent the reaction rates by the length + of edges. Short edges are fast rates. It doesn't look very good + usually. + use_pretty_print : bool + This uses the "pretty print" function to represent reactions + and nodes a bit cleaner. + pp_show_material : bool + Default false because this is listed in "type". + pp_show_rates : bool + Default true because this is useful information. + pp_show_attributes : bool + colordict : dict + A dictionary containing which node types are what color based + upon the following keywords: Keywords are chosen to match species.material_type {"complex": "cyan", @@ -384,24 +397,27 @@ def generate_networkx_graph( "phosphate": "yellow", "nothing":"purple"} - When using a custom colordict, the following attributes will be checked to - find colors with the first keys taking precedence: + When using a custom colordict, the following attributes will be + checked to find colors with the first keys taking precedence: repr(species): "color" species.name: "color" (species.material_type, tuple(species.attributes)): "color" species.material_type: "color" tuple(species.attributes): "color" - - imagedict is a dictionary which contains species and their corresponding + imagedict : dict + A dictionary which contains species and their corresponding image representations. This is the output generated by - CRNPlotter.renderMixture() + CRNPlotter.renderMixture(). - output: - ================== - CRNgraph: the DiGraph object containing all nodes and edges - CRNspeciesonly: a DiGraph object with only species - CRNreactionsonly: a DiGraph object with only reactions + Returns + ------- + CRNgraph : DiGraph + The DiGraph object containing all nodes and edges. + CRNspeciesonly : DiGraph + A DiGraph object with only species. + CRNreactionsonly : DiGraph + A DiGraph object with only reactions. """ if not PLOT_NETWORK: @@ -598,8 +614,8 @@ def get_directed(self, direction, bound=None, non_binder=None): """Copy of self with direction changed. In the case of MultiPart it also means reversing the order of the - subparts. A MultiPart binds to things differently from a normal - part. the binding is distributed among the subparts. "non_binder" + subparts. A MultiPart binds to things differently from a normal + part. The binding is distributed among the subparts. 'non_binder' indicates a dpl_type which should not be shown binding to things. """ @@ -675,7 +691,7 @@ def __init__( label_size=13, added_opts=None, ): - """Simplified DNAconstruct with only plotting information.""" + """Simplified DNA_construct with only plotting information.""" self.name = name self.parts_list = parts_list self.circular = circular @@ -695,9 +711,9 @@ def get_dpl(self): return outlist def get_dpl_binders(self): - """Output a dnaplotlib dictionary list to represent the "binders". + """Output a dnaplotlib dictionary list to represent 'binders'. - Binders are "regulation" arcs modified to draw a SBOL glyph + Binders are 'regulation' arcs modified to draw a SBOL glyph instead of a line. """ @@ -1297,10 +1313,10 @@ def render_network_bokeh( species_glyph_size=12, reaction_glyph_size=8, export_name=None, - **keywords, + **kwargs, ): DG, DGspec, DGrxn = generate_networkx_graph( - CRN, **keywords + CRN, **kwargs ) # this creates the networkx objects plot = Plot( width=500, @@ -1309,7 +1325,7 @@ def render_network_bokeh( y_range=Range1d(-500, 500), ) # this generates a show_im = False - if 'imagedict' in keywords and keywords['imagedict'] is not None: + if 'imagedict' in kwargs and kwargs['imagedict'] is not None: show_im = True if export: plot.output_backend = 'svg' diff --git a/biocrnpyler/utils/sbmlutil.py b/biocrnpyler/utils/sbmlutil.py index 06a6b16b..d53fe198 100644 --- a/biocrnpyler/utils/sbmlutil.py +++ b/biocrnpyler/utils/sbmlutil.py @@ -35,16 +35,24 @@ def create_sbml_model( """Creates SBML Level 3 Version 2 model with some fixed standard settings. Refer to python-libsbml for more information on SBML API. - :param compartment_id: - :param time_units: - :param extent_units: - :param substance_units: - :param length_units: - :param area_units: - :param volume_units: - :param volume: - :param model_id: - :return: the SBMLDocument and the Model object as a tuple + + Parameters + ---------- + compartment_id : str + time_units : str + extent_units : str + substance_units : str + length_units : str + area_units : str + volume_units : str + volume : float + model_id : str + + Returns + ------- + tuple + The SBMLDocument and the Model object as a tuple. + """ document = libsbml.SBMLDocument(3, 2) model = document.createModel() @@ -92,13 +100,17 @@ def add_all_species( ): """Adds a list of Species to the SBML model. - :param model: valid SBML model - :param species: list of species to be added to the SBML model - :param compartment: compartment id, if empty species go to the first - compartment - :param initial_concentration_dict: a dictionary s --> - initial_concentration - :return: None + Parameters + ---------- + model : libsbml.Model + Valid SBML model. + species : list + List of species to be added to the SBML model. + initial_condition_dictionary : dict + A dictionary s --> initial_concentration. + compartment : str, optional + Compartment id, if empty species go to the first compartment. + """ elementlist = model.getSBMLDocument().getListOfAllElements() @@ -144,12 +156,21 @@ def add_species( ): """Helper function to add a species to the sbml model. - :param model: - :param compartment: a compartment in the SBML model - :param species: must be chemical_reaction_network.species objects - :param initial_concentration: initial concentration of the species - in the SBML model - :return: SBML species object + Parameters + ---------- + model : libsbml.Model + compartment : libsbml.Compartment + A compartment in the SBML model. + species_name : str + species_id : str + initial_concentration : float, optional + Initial concentration of the species in the SBML model. + + Returns + ------- + libsbml.Species + SBML species object. + """ # Construct the species ID @@ -169,23 +190,36 @@ def add_species( return sbml_species -def add_all_compartments(model, compartments: List, **keywords): +def add_all_compartments(model, compartments: List, **kwargs): """Adds the list of Compartment objects to the SBML model. - :param model: valid SBML model - :param compartments: list of compartments to be added to the SBML model - :return: None + Parameters + ---------- + model : libsbml.Model + Valid SBML model. + compartments : list + List of compartments to be added to the SBML model. + """ for compartment in compartments: - add_compartment(model=model, compartment=compartment, **keywords) + add_compartment(model=model, compartment=compartment, **kwargs) -def add_compartment(model, compartment, **keywords): +def add_compartment(model, compartment, **kwargs): """Helper function to add a compartment to the SBML model. - :param model: a valid SBML model - :param compartment: a Compartment object - :return: SBML compartment object + Parameters + ---------- + model : libsbml.Model + A valid SBML model. + compartment : bcp.Compartment + A Compartment object. + + Returns + ------- + libsbml.Compartment + SBML compartment object. + """ sbml_compartment = model.createCompartment() compartment_id = compartment.name @@ -235,10 +269,15 @@ def find_parameter(mixture, id): def add_all_reactions(model, reactions: List, stochastic=False, **kwargs): """Adds a list of reactions to the SBML model. - :param model: an sbml model created by create_sbml_model() - :param reactions: list of Reactions - :param stochastic: binary flag for stochastic models - :return: None + Parameters + ---------- + model : libsbml.Model + An sbml model created by create_sbml_model(). + reactions : list + List of Reactions. + stochastic : bool + Binary flag for stochastic models. + """ elementlist = model.getSBMLDocument().getListOfAllElements() @@ -286,12 +325,23 @@ def add_reaction( ): """Adds a sbml_reaction to an sbml model. - :param model: an sbml model created by create_sbml_model() - :param crn_reaction: must be a chemical_reaction_network.reaction object - :param reaction_id: unique id of the reaction - :param stochastic: stochastic model flag - :param reverse_reaction: - :return: SBML Reaction object + Parameters + ---------- + model : libsbml.Model + An sbml model created by create_sbml_model(). + crn_reaction : bcp.Reaction + Must be a chemical_reaction_network.reaction object. + reaction_id : str + Unique id of the reaction. + stochastic : bool + Stochastic model flag. + reverse_reaction : bool + + Returns + ------- + libsbml.Reaction + SBML Reaction object. + """ # Create the sbml_reaction in SBML sbml_reaction = model.createReaction() @@ -568,8 +618,11 @@ def getAllIds(allElements): def getSpeciesByName(model, name, compartment=''): """Returns a list of species in the Model with the given name. - compartment : (Optional) argument to specify the compartment name in which - to look for the species. + Parameters + ---------- + compartment : str, optional + Compartment name in which to look for the species. + """ if not isinstance(name, str): raise ValueError(f"'name' must be a string. Received {name}.") @@ -598,25 +651,23 @@ def getSpeciesByName(model, name, compartment=''): # Validate SBML - - class validateSBML(object): - """libSBML class to validate the generated SBML models. - - ## @brief Validates SBMLDocument - ## @author Akiya Jouraku (translated from libSBML C++ examples) - ## @author Ben Bornstein - ## @author Michael Hucka - """ + """Class to validate the generated SBML models.""" def __init__(self, ucheck): self.reader = libsbml.SBMLReader() self.ucheck = ucheck def validate(self, sbml_document, print_results=False): - """sbml_document: libSBML SBMLDocument object. + """Validate an SBML document. + + Parameters + ---------- + sbml_document : libsbml.SBMLDocument + libSBML SBMLDocument object. + print_results : bool + Print toggle for validation warnings. - print_results: Print toggle for validation warnings. """ sbmlDoc = sbml_document errors = sbmlDoc.getNumErrors() diff --git a/biocrnpyler/utils/units.py b/biocrnpyler/utils/units.py index 6cdebccd..43dcbe68 100644 --- a/biocrnpyler/utils/units.py +++ b/biocrnpyler/utils/units.py @@ -128,9 +128,19 @@ def biocrnpyler_supported_units(): def create_new_unit_definition(model, unit_id): - """Creates UnitDefinition inside SBML Model object. + """ + Creates UnitDefinition inside SBML Model object. + + Parameters + ---------- + model : libsbml.Model + unit_id : str + + Returns + ------- + libsbml.UnitDefinition + A pointer to the new libSBML object created for the unit type. - Returns a pointer to the new libSBML object created for the unit type. """ supported_units = biocrnpyler_supported_units() if not isinstance(unit_id, str): diff --git a/docs/conf.py b/docs/conf.py index bfbecc1f..e0be1ec6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,6 +54,7 @@ 'nbsphinx', 'nbsphinx_link', 'recommonmark', + 'numpydoc', ] source_suffix = ['.rst'] @@ -66,11 +67,12 @@ autodoc_default_options = { 'members': True, 'inherited-members': True, + 'special-members': True, 'exclude-members': '__init__, __weakref__, __repr__, __str__', } # For classes, include both the class docstring and the init docstring -autoclass_content = 'both' +autoclass_content = 'class' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -80,6 +82,11 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build'] +# Don't automatically show all members of class in Methods & Attributes section +numpydoc_show_class_members = False + +# Don't create a Sphinx TOC for the lists of class methods and attributes +numpydoc_class_members_toctree = False # -- Options for HTML output ------------------------------------------------- @@ -172,13 +179,14 @@ def linkcode_resolve(domain, info): linespec = '' base_url = "https://github.com/BuildACell/BioCRNPyler/blob/" - if release != version: # development release + if release != version: # development release # TODO: replace 'refactor-modules' with 'master' -> replaced with main # print(" --> ", base_url + "refactor-modules/control/%s%s" % (fn, linespec)) return base_url + 'main/biocrnpyler/%s%s' % (fn, linespec) - else: # specific version + else: # specific version return base_url + '%s/biocrnpyler/%s%s' % (version, fn, linespec) + # -- Options for doctest ---------------------------------------------- # Import biocrnpyler as bcp diff --git a/docs/examples/1. Combinatorial Promoters.ipynb b/docs/examples/1. Combinatorial Promoters.ipynb new file mode 120000 index 00000000..8c00cb64 --- /dev/null +++ b/docs/examples/1. Combinatorial Promoters.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/1. Combinatorial Promoters.ipynb \ No newline at end of file diff --git a/docs/examples/2. Membrane Models.ipynb b/docs/examples/2. Membrane Models.ipynb new file mode 120000 index 00000000..5e31db24 --- /dev/null +++ b/docs/examples/2. Membrane Models.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/2. Membrane Models.ipynb \ No newline at end of file diff --git a/docs/examples/3. Multiple Occupancy in TX-TL.ipynb b/docs/examples/3. Multiple Occupancy in TX-TL.ipynb new file mode 120000 index 00000000..19847749 --- /dev/null +++ b/docs/examples/3. Multiple Occupancy in TX-TL.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/3. Multiple Occupancy in TX-TL.ipynb \ No newline at end of file diff --git a/docs/examples/4. Combinatorial Conformation Modeling.ipynb b/docs/examples/4. Combinatorial Conformation Modeling.ipynb new file mode 120000 index 00000000..535de5ec --- /dev/null +++ b/docs/examples/4. Combinatorial Conformation Modeling.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/4. Combinatorial Conformation Modeling.ipynb \ No newline at end of file diff --git a/docs/examples/5. TX-TL Toolbox.ipynb b/docs/examples/5. TX-TL Toolbox.ipynb new file mode 120000 index 00000000..6f984be1 --- /dev/null +++ b/docs/examples/5. TX-TL Toolbox.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/5. TX-TL Toolbox.ipynb \ No newline at end of file diff --git a/docs/examples/6. Integrase Examples.ipynb b/docs/examples/6. Integrase Examples.ipynb new file mode 120000 index 00000000..66962463 --- /dev/null +++ b/docs/examples/6. Integrase Examples.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/6. Integrase Examples.ipynb \ No newline at end of file diff --git a/docs/examples/7. Transport_Models.ipynb b/docs/examples/7. Transport_Models.ipynb new file mode 120000 index 00000000..c12f4e25 --- /dev/null +++ b/docs/examples/7. Transport_Models.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/7. Transport_Models.ipynb \ No newline at end of file diff --git a/docs/examples/8. Multicellular_Transport.ipynb b/docs/examples/8. Multicellular_Transport.ipynb new file mode 120000 index 00000000..99508721 --- /dev/null +++ b/docs/examples/8. Multicellular_Transport.ipynb @@ -0,0 +1 @@ +../../examples/Specialized Tutorials/8. Multicellular_Transport.ipynb \ No newline at end of file diff --git a/examples/6. Global Mechanisms.ipynb b/examples/6. Global Mechanisms.ipynb index 04032e86..b3eeb94e 100644 --- a/examples/6. Global Mechanisms.ipynb +++ b/examples/6. Global Mechanisms.ipynb @@ -539,7 +539,7 @@ "gamS = Species(\"GamS\")\n", "\n", "linear_dna_degradation = Deg_Tagged_Degradation(\n", - " degredase=recBCD, filter_dict={\"dna\":True, \"circular\":False}, default_on=False)\n", + " degradase=recBCD, filter_dict={\"dna\":True, \"circular\":False}, default_on=False)\n", "\n", "inhibited_recBCD = ChemicalComplex([recBCD]+2*[gamS])\n", "\n", diff --git a/pyproject.toml b/pyproject.toml index 1f0465d0..b30ba757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,12 +74,13 @@ ignore = [ 'D100', 'D101', 'D102', # ignore missing docstrings for now 'D103', 'D104', 'D105', 'D105', 'D106', 'D107', + 'D401', # ignore imperative docstring checking for now 'D417', # ignore missing argument descriptions for now ] [tool.ruff.lint.per-file-ignores] # Ignore 'D' and 'E' rules everywhere except for the `biocrnpyler/` directory. -'!biocrnpyler/**.py' = ['D', 'E'] +'!biocrnpyler/**.py' = ['D', 'E', 'I'] 'Tests/**.py' = [ 'E', # ignore code style 'F841', # unused variables OK @@ -95,7 +96,7 @@ ignore = [ quote-style = 'preserve' [tool.ruff.lint.pydocstyle] -convention = "google" +convention = "numpy" [tool.isort] profile = 'black'