"""
Functions to initialize molecule states from sim_data.
"""
import numpy as np
import numpy.typing as npt
from numpy.lib import recfunctions as rfn
from typing import Any
from unum import Unum
from ecoli.library.schema import attrs, bulk_name_to_idx, create_unique_indexes, counts
from ecoli.processes.polypeptide_elongation import (
calculate_trna_charging,
REMOVED_FROM_CHARGING,
MICROMOLAR_UNITS,
)
from wholecell.utils import units
from wholecell.utils.fitting import (
countsFromMassAndExpression,
masses_and_counts_for_homeostatic_target,
normalize,
)
try:
from wholecell.utils.mc_complexation import mccFormComplexesWithPrebuiltMatrices
except ImportError as exc:
raise RuntimeError(
"Failed to import Cython module. Try running 'make clean compile'."
) from exc
from wholecell.utils.polymerize import computeMassIncrease
from wholecell.utils.random import stochasticRound
RAND_MAX = 2**31
[docs]
def create_bulk_container(
sim_data,
n_seeds=1,
condition=None,
seed=0,
ppgpp_regulation=True,
trna_attenuation=True,
mass_coeff=1,
form_complexes=True,
):
try:
old_condition = sim_data.condition
if condition is not None:
sim_data.condition = condition
media_id = sim_data.conditions[sim_data.condition]["nutrients"]
exchange_data = sim_data.external_state.exchange_data_from_media(media_id)
import_molecules = set(
exchange_data["importUnconstrainedExchangeMolecules"]
) | set(exchange_data["importConstrainedExchangeMolecules"])
random_state = np.random.RandomState(seed=seed)
# Construct bulk container
ids_molecules = sim_data.internal_state.bulk_molecules.bulk_data["id"]
average_container = np.array(
[mol_data for mol_data in zip(ids_molecules, np.zeros(len(ids_molecules)))],
dtype=[("id", ids_molecules.dtype), ("count", np.float64)],
)
for n in range(n_seeds):
random_state = np.random.RandomState(seed=seed + n)
average_container["count"] += initialize_bulk_counts(
sim_data,
media_id,
import_molecules,
random_state,
mass_coeff,
ppgpp_regulation,
trna_attenuation,
form_complexes=form_complexes,
)["count"]
except Exception:
raise RuntimeError(
"sim_data might not be fully initialized. "
"Make sure all attributes have been set before "
"using this function."
)
sim_data.condition = old_condition
average_container["count"] = average_container["count"] / n_seeds
return average_container
[docs]
def initialize_bulk_counts(
sim_data,
media_id,
import_molecules,
random_state,
mass_coeff,
ppgpp_regulation,
trna_attenuation,
form_complexes=True,
):
# Allocate count array to populate
bulk_counts = np.zeros(
len(sim_data.internal_state.bulk_molecules.bulk_data["id"]), dtype=int
)
# Set protein counts from expression
initialize_protein_monomers(
bulk_counts,
sim_data,
random_state,
mass_coeff,
ppgpp_regulation,
trna_attenuation,
)
# Set RNA counts from expression
initialize_rna(
bulk_counts,
sim_data,
random_state,
mass_coeff,
ppgpp_regulation,
trna_attenuation,
)
# Set mature RNA counts
initialize_mature_RNA(bulk_counts, sim_data)
# Set other biomass components
set_small_molecule_counts(
bulk_counts, sim_data, media_id, import_molecules, mass_coeff
)
# Form complexes
if form_complexes:
initialize_complexation(bulk_counts, sim_data, random_state)
bulk_masses = sim_data.internal_state.bulk_molecules.bulk_data["mass"].asNumber(
units.fg / units.mol
) / sim_data.constants.n_avogadro.asNumber(1 / units.mol)
bulk_submasses = []
bulk_submass_dtypes = []
for submass, idx in sim_data.submass_name_to_index.items():
bulk_submasses.append(bulk_masses[:, idx])
bulk_submass_dtypes.append((f"{submass}_submass", np.float64))
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data.struct_array["id"]
bulk_array = np.array(
[mol_data for mol_data in zip(bulk_ids, bulk_counts, *bulk_submasses)],
dtype=[("id", bulk_ids.dtype), ("count", int)] + bulk_submass_dtypes,
)
return bulk_array
[docs]
def initialize_unique_molecules(
bulk_state,
sim_data,
cell_mass,
random_state,
unique_id_rng,
superhelical_density,
ppgpp_regulation,
trna_attenuation,
mechanistic_replisome,
):
unique_molecules = {}
# Initialize counts of full chromosomes
initialize_full_chromosome(unique_molecules, sim_data, unique_id_rng)
# Initialize unique molecules relevant to replication
initialize_replication(
bulk_state,
unique_molecules,
sim_data,
cell_mass,
mechanistic_replisome,
unique_id_rng,
)
# Initialize bound transcription factors
initialize_transcription_factors(
bulk_state, unique_molecules, sim_data, random_state
)
# Initialize active RNAPs and unique molecule representations of RNAs
initialize_transcription(
bulk_state,
unique_molecules,
sim_data,
random_state,
unique_id_rng,
ppgpp_regulation,
trna_attenuation,
)
# Initialize linking numbers of chromosomal segments
if superhelical_density:
initialize_chromosomal_segments(unique_molecules, sim_data, unique_id_rng)
else:
unique_molecules["chromosomal_segment"] = create_new_unique_molecules(
"chromosomal_segment", 0, sim_data, unique_id_rng
)
# Initialize active ribosomes
initialize_translation(
bulk_state, unique_molecules, sim_data, random_state, unique_id_rng
)
return unique_molecules
[docs]
def create_new_unique_molecules(name, n_mols, sim_data, random_state, **attrs):
"""
Helper function to create a new Numpy structured array with n_mols
instances of the unique molecule called name. Accepts keyword arguments
that become initial values for specified attributes of the new molecules.
"""
dtypes = list(
sim_data.internal_state.unique_molecule.unique_molecule_definitions[
name
].items()
)
submasses = list(sim_data.submass_name_to_index)
dtypes += [(f"massDiff_{submass}", "<f8") for submass in submasses]
dtypes += [("_entryState", "i1"), ("unique_index", "<i8")]
unique_mols = np.zeros(n_mols, dtype=dtypes)
for attr_name, attr_value in attrs.items():
unique_mols[attr_name] = attr_value
unique_mols["unique_index"] = create_unique_indexes(n_mols, random_state)
unique_mols["_entryState"] = 1
return unique_mols
[docs]
def initialize_protein_monomers(
bulk_counts, sim_data, random_state, mass_coeff, ppgpp_regulation, trna_attenuation
):
monomer_mass = (
mass_coeff
* sim_data.mass.get_component_masses(
sim_data.condition_to_doubling_time[sim_data.condition]
)["proteinMass"]
/ sim_data.mass.avg_cell_to_initial_cell_conversion_factor
)
# TODO: unify this logic with the parca so it doesn]t fall out of step
# again (look at teh calProteinCounts function)
transcription = sim_data.process.transcription
if ppgpp_regulation:
rna_expression = sim_data.calculate_ppgpp_expression(sim_data.condition)
else:
rna_expression = transcription.rna_expression[sim_data.condition]
if trna_attenuation:
# Need to adjust expression (calculated without attenuation) by basal_adjustment
# to get the expected expression without any attenuation and then multiply
# by the condition readthrough probability to get the condition specific expression
readthrough = transcription.attenuation_readthrough[sim_data.condition]
basal_adjustment = transcription.attenuation_readthrough["basal"]
rna_expression[transcription.attenuated_rna_indices] *= (
readthrough / basal_adjustment
)
monomer_expression = normalize(
sim_data.process.transcription.cistron_tu_mapping_matrix.dot(rna_expression)[
sim_data.relation.cistron_to_monomer_mapping
]
* sim_data.process.translation.translation_efficiencies_by_monomer
/ (
np.log(2)
/ sim_data.condition_to_doubling_time[sim_data.condition].asNumber(units.s)
+ sim_data.process.translation.monomer_data["deg_rate"].asNumber(
1 / units.s
)
)
)
n_monomers = countsFromMassAndExpression(
monomer_mass.asNumber(units.g),
sim_data.process.translation.monomer_data["mw"].asNumber(units.g / units.mol),
monomer_expression,
sim_data.constants.n_avogadro.asNumber(1 / units.mol),
)
# Get indices for monomers in bulk counts array
monomer_ids = sim_data.process.translation.monomer_data["id"]
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data["id"]
monomer_idx = bulk_name_to_idx(monomer_ids, bulk_ids)
# Calculate initial counts of each monomer from mutinomial distribution
bulk_counts[monomer_idx] = random_state.multinomial(n_monomers, monomer_expression)
[docs]
def initialize_rna(
bulk_counts, sim_data, random_state, mass_coeff, ppgpp_regulation, trna_attenuation
):
"""
Initializes counts of RNAs in the bulk molecule container using RNA
expression data. mRNA counts are also initialized here, but is later reset
to zero when the representations for mRNAs are moved to the unique molecule
container.
"""
transcription = sim_data.process.transcription
rna_mass = (
mass_coeff
* sim_data.mass.get_component_masses(
sim_data.condition_to_doubling_time[sim_data.condition]
)["rnaMass"]
/ sim_data.mass.avg_cell_to_initial_cell_conversion_factor
)
if ppgpp_regulation:
rna_expression = sim_data.calculate_ppgpp_expression(sim_data.condition)
else:
rna_expression = normalize(transcription.rna_expression[sim_data.condition])
if trna_attenuation:
# Need to adjust expression (calculated without attenuation) by basal_adjustment
# to get the expected expression without any attenuation and then multiply
# by the condition readthrough probability to get the condition specific expression
readthrough = transcription.attenuation_readthrough[sim_data.condition]
basal_adjustment = transcription.attenuation_readthrough["basal"]
rna_expression[transcription.attenuated_rna_indices] *= (
readthrough / basal_adjustment
)
rna_expression /= rna_expression.sum()
n_rnas = countsFromMassAndExpression(
rna_mass.asNumber(units.g),
transcription.rna_data["mw"].asNumber(units.g / units.mol),
rna_expression,
sim_data.constants.n_avogadro.asNumber(1 / units.mol),
)
# Get indices for monomers in bulk counts array
rna_ids = transcription.rna_data["id"]
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data["id"]
rna_idx = bulk_name_to_idx(rna_ids, bulk_ids)
# Calculate initial counts of each RNA from mutinomial distribution
bulk_counts[rna_idx] = random_state.multinomial(n_rnas, rna_expression)
[docs]
def initialize_mature_RNA(bulk_counts, sim_data):
"""
Initializes counts of mature RNAs in the bulk molecule container using the
counts of unprocessed RNAs. Also consolidates the different variants of each
rRNA molecule into the main type.
"""
transcription = sim_data.process.transcription
rna_data = transcription.rna_data
unprocessed_rna_ids = rna_data["id"][rna_data["is_unprocessed"]]
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data["id"]
unprocessed_rna_idx = bulk_name_to_idx(unprocessed_rna_ids, bulk_ids)
# Skip if there are no unprocessed RNAs represented
if len(unprocessed_rna_ids) > 0:
mature_rna_ids = transcription.mature_rna_data["id"]
maturation_stoich_matrix = transcription.rna_maturation_stoich_matrix
mature_rna_idx = bulk_name_to_idx(mature_rna_ids, bulk_ids)
# Get counts of unprocessed RNAs
unprocessed_rna_counts = bulk_counts[unprocessed_rna_idx]
# Assume all unprocessed RNAs are converted to mature RNAs
bulk_counts[unprocessed_rna_idx] = 0
bulk_counts[mature_rna_idx] += maturation_stoich_matrix.dot(
unprocessed_rna_counts
)
# Get IDs of rRNAs
main_23s_rRNA_id = sim_data.molecule_groups.s50_23s_rRNA[0]
main_16s_rRNA_id = sim_data.molecule_groups.s30_16s_rRNA[0]
main_5s_rRNA_id = sim_data.molecule_groups.s50_5s_rRNA[0]
variant_23s_rRNA_ids = sim_data.molecule_groups.s50_23s_rRNA[1:]
variant_16s_rRNA_ids = sim_data.molecule_groups.s30_16s_rRNA[1:]
variant_5s_rRNA_ids = sim_data.molecule_groups.s50_5s_rRNA[1:]
# Get indices of main and variant rRNAs
main_23s_rRNA_idx = bulk_name_to_idx(main_23s_rRNA_id, bulk_ids)
main_16s_rRNA_idx = bulk_name_to_idx(main_16s_rRNA_id, bulk_ids)
main_5s_rRNA_idx = bulk_name_to_idx(main_5s_rRNA_id, bulk_ids)
variant_23s_rRNA_idx = bulk_name_to_idx(variant_23s_rRNA_ids, bulk_ids)
variant_16s_rRNA_idx = bulk_name_to_idx(variant_16s_rRNA_ids, bulk_ids)
variant_5s_rRNA_idx = bulk_name_to_idx(variant_5s_rRNA_ids, bulk_ids)
# Evolve states
bulk_counts[main_23s_rRNA_idx] += bulk_counts[variant_23s_rRNA_idx].sum()
bulk_counts[main_16s_rRNA_idx] += bulk_counts[variant_16s_rRNA_idx].sum()
bulk_counts[main_5s_rRNA_idx] += bulk_counts[variant_5s_rRNA_idx].sum()
bulk_counts[variant_23s_rRNA_idx] -= bulk_counts[variant_23s_rRNA_idx]
bulk_counts[variant_16s_rRNA_idx] -= bulk_counts[variant_16s_rRNA_idx]
bulk_counts[variant_5s_rRNA_idx] -= bulk_counts[variant_5s_rRNA_idx]
# TODO: remove checks for zero concentrations (change to assertion)
# TODO: move any rescaling logic to KB/fitting
[docs]
def set_small_molecule_counts(
bulk_counts, sim_data, media_id, import_molecules, mass_coeff, cell_mass=None
):
doubling_time = sim_data.condition_to_doubling_time[sim_data.condition]
conc_dict = sim_data.process.metabolism.concentration_updates.concentrations_based_on_nutrients(
media_id=media_id, imports=import_molecules
)
conc_dict.update(sim_data.mass.getBiomassAsConcentrations(doubling_time))
conc_dict[sim_data.molecule_ids.ppGpp] = (
sim_data.growth_rate_parameters.get_ppGpp_conc(doubling_time)
)
molecule_ids = sorted(conc_dict)
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data["id"]
molecule_concentrations = (units.mol / units.L) * np.array(
[conc_dict[key].asNumber(units.mol / units.L) for key in molecule_ids]
)
if cell_mass is None:
avg_cell_fraction_mass = sim_data.mass.get_component_masses(doubling_time)
other_dry_mass = (
mass_coeff
* (
avg_cell_fraction_mass["proteinMass"]
+ avg_cell_fraction_mass["rnaMass"]
+ avg_cell_fraction_mass["dnaMass"]
)
/ sim_data.mass.avg_cell_to_initial_cell_conversion_factor
)
else:
small_molecule_mass = 0 * units.fg
for mol in conc_dict:
mol_idx = bulk_name_to_idx(mol, bulk_ids)
small_molecule_mass += (
bulk_counts[mol_idx]
* sim_data.getter.get_mass(mol)
/ sim_data.constants.n_avogadro
)
other_dry_mass = cell_mass - small_molecule_mass
masses_to_add, counts_to_add = masses_and_counts_for_homeostatic_target(
other_dry_mass,
molecule_concentrations,
sim_data.getter.get_masses(molecule_ids),
sim_data.constants.cell_density,
sim_data.constants.n_avogadro,
)
molecule_idx = bulk_name_to_idx(molecule_ids, bulk_ids)
bulk_counts[molecule_idx] = counts_to_add
[docs]
def initialize_complexation(bulk_counts, sim_data, random_state):
molecule_names = sim_data.process.complexation.molecule_names
bulk_ids = sim_data.internal_state.bulk_molecules.bulk_data["id"]
molecule_idx = bulk_name_to_idx(molecule_names, bulk_ids)
stoich_matrix = sim_data.process.complexation.stoich_matrix().astype(
np.int64, order="F"
)
molecule_counts = bulk_counts[molecule_idx]
updated_molecule_counts, complexation_events = mccFormComplexesWithPrebuiltMatrices(
molecule_counts,
random_state.randint(1000),
stoich_matrix,
*sim_data.process.complexation.prebuilt_matrices,
)
bulk_counts[molecule_idx] = updated_molecule_counts
if np.any(updated_molecule_counts < 0):
raise ValueError("Negative counts after complexation")
[docs]
def initialize_full_chromosome(unique_molecules, sim_data, unique_id_rng):
"""
Initializes the counts of full chromosomes to one. The division_time of
this initial chromosome is set to be zero for consistency.
"""
unique_molecules["full_chromosome"] = create_new_unique_molecules(
"full_chromosome",
1,
sim_data,
unique_id_rng,
division_time=0.0,
has_triggered_division=True,
domain_index=0,
)
[docs]
def initialize_replication(
bulk_state,
unique_molecules,
sim_data,
cell_mass,
mechanistic_replisome,
unique_id_rng,
):
"""
Initializes replication by creating an appropriate number of replication
forks given the cell growth rate. This also initializes the gene dosage
bulk counts using the initial locations of the forks.
"""
# Determine the number and location of replication forks at the start of
# the cell cycle
# Get growth rate constants
tau = sim_data.condition_to_doubling_time[sim_data.condition].asUnit(units.min)
critical_mass = sim_data.mass.get_dna_critical_mass(tau)
replication_rate = sim_data.process.replication.basal_elongation_rate
# Calculate length of replichore
genome_length = sim_data.process.replication.genome_length
replichore_length = np.ceil(0.5 * genome_length) * units.nt
# Calculate the maximum number of replisomes that could be formed with
# the existing counts of replisome subunits. If mechanistic_replisome option
# is off, set to an arbitrary high number.
replisome_trimer_idx = bulk_name_to_idx(
sim_data.molecule_groups.replisome_trimer_subunits, bulk_state["id"]
)
replisome_monomer_idx = bulk_name_to_idx(
sim_data.molecule_groups.replisome_monomer_subunits, bulk_state["id"]
)
if mechanistic_replisome:
n_max_replisomes = np.min(
np.concatenate(
(
bulk_state["count"][replisome_trimer_idx] // 3,
bulk_state["count"][replisome_monomer_idx],
)
)
)
else:
n_max_replisomes = 1000
# Generate arrays specifying appropriate initial replication conditions
oric_state, replisome_state, domain_state = determine_chromosome_state(
tau,
replichore_length,
n_max_replisomes,
sim_data.process.replication.no_child_place_holder,
cell_mass,
critical_mass,
replication_rate,
)
n_oric = oric_state["domain_index"].size
n_replisome = replisome_state["domain_index"].size
n_domain = domain_state["domain_index"].size
# Add OriC molecules with the proposed attributes
unique_molecules["oriC"] = create_new_unique_molecules(
"oriC", n_oric, sim_data, unique_id_rng, domain_index=oric_state["domain_index"]
)
# Add chromosome domain molecules with the proposed attributes
unique_molecules["chromosome_domain"] = create_new_unique_molecules(
"chromosome_domain",
n_domain,
sim_data,
unique_id_rng,
domain_index=domain_state["domain_index"],
child_domains=domain_state["child_domains"],
)
if n_replisome != 0:
# Update mass of replisomes if the mechanistic replisome option is set
if mechanistic_replisome:
replisome_trimer_subunit_masses = np.vstack(
[
sim_data.getter.get_submass_array(x).asNumber(
units.fg / units.count
)
for x in sim_data.molecule_groups.replisome_trimer_subunits
]
)
replisome_monomer_subunit_masses = np.vstack(
[
sim_data.getter.get_submass_array(x).asNumber(
units.fg / units.count
)
for x in sim_data.molecule_groups.replisome_monomer_subunits
]
)
replisome_mass_array = 3 * replisome_trimer_subunit_masses.sum(
axis=0
) + replisome_monomer_subunit_masses.sum(axis=0)
replisome_protein_mass = replisome_mass_array.sum()
else:
replisome_protein_mass = 0.0
# Update mass to account for DNA strands that have already been
# elongated.
sequences = sim_data.process.replication.replication_sequences
fork_coordinates = replisome_state["coordinates"]
sequence_elongations = np.abs(np.repeat(fork_coordinates, 2))
mass_increase_dna = computeMassIncrease(
np.tile(sequences, (n_replisome // 2, 1)),
sequence_elongations,
sim_data.process.replication.replication_monomer_weights.asNumber(units.fg),
)
# Add active replisomes as unique molecules and set attributes
unique_molecules["active_replisome"] = create_new_unique_molecules(
"active_replisome",
n_replisome,
sim_data,
unique_id_rng,
domain_index=replisome_state["domain_index"],
coordinates=replisome_state["coordinates"],
right_replichore=replisome_state["right_replichore"],
massDiff_DNA=mass_increase_dna[0::2] + mass_increase_dna[1::2],
massDiff_protein=replisome_protein_mass,
)
if mechanistic_replisome:
# Remove replisome subunits from bulk molecules
bulk_state["count"][replisome_trimer_idx] -= 3 * n_replisome
bulk_state["count"][replisome_monomer_idx] -= n_replisome
else:
# For n_replisome = 0, still create an empty structured array with
# the expected fields
unique_molecules["active_replisome"] = create_new_unique_molecules(
"active_replisome", n_replisome, sim_data, unique_id_rng
)
# Get coordinates of all genes, promoters and DnaA boxes
all_gene_coordinates = sim_data.process.transcription.cistron_data[
"replication_coordinate"
]
all_promoter_coordinates = sim_data.process.transcription.rna_data[
"replication_coordinate"
]
all_DnaA_box_coordinates = sim_data.process.replication.motif_coordinates[
"DnaA_box"
]
# Define function that initializes attributes of sequence motifs given the
# initial state of the chromosome
def get_motif_attributes(all_motif_coordinates):
"""
Using the initial positions of replication forks, calculate attributes
of unique molecules representing DNA motifs, given their positions on
the genome.
Args:
all_motif_coordinates (ndarray): Genomic coordinates of DNA motifs,
represented in a specific order
Returns:
motif_index: Indices of all motif copies, in the case where
different indexes imply a different functional role
motif_coordinates: Genomic coordinates of all motif copies
motif_domain_index: Domain indexes of the chromosome domain that
each motif copy belongs to
"""
motif_index, motif_coordinates, motif_domain_index = [], [], []
def in_bounds(coordinates, lb, ub):
return np.logical_and(coordinates < ub, coordinates > lb)
# Loop through all chromosome domains
for domain_idx in domain_state["domain_index"]:
# If the domain is the mother domain of the initial chromosome,
if domain_idx == 0:
if n_replisome == 0:
# No replisomes - all motifs should fall in this domain
motif_mask = np.ones_like(all_motif_coordinates, dtype=bool)
else:
# Get domain boundaries
domain_boundaries = replisome_state["coordinates"][
replisome_state["domain_index"] == 0
]
# Add motifs outside of this boundary
motif_mask = np.logical_or(
all_motif_coordinates > domain_boundaries.max(),
all_motif_coordinates < domain_boundaries.min(),
)
# If the domain contains the origin,
elif np.isin(domain_idx, oric_state["domain_index"]):
# Get index of the parent domain
parent_domain_idx = domain_state["domain_index"][
np.where(domain_state["child_domains"] == domain_idx)[0]
]
# Get domain boundaries of the parent domain
parent_domain_boundaries = replisome_state["coordinates"][
replisome_state["domain_index"] == parent_domain_idx
]
# Add motifs inside this boundary
motif_mask = in_bounds(
all_motif_coordinates,
parent_domain_boundaries.min(),
parent_domain_boundaries.max(),
)
# If the domain neither contains the origin nor the terminus,
else:
# Get index of the parent domain
parent_domain_idx = domain_state["domain_index"][
np.where(domain_state["child_domains"] == domain_idx)[0]
]
# Get domain boundaries of the parent domain
parent_domain_boundaries = replisome_state["coordinates"][
replisome_state["domain_index"] == parent_domain_idx
]
# Get domain boundaries of this domain
domain_boundaries = replisome_state["coordinates"][
replisome_state["domain_index"] == domain_idx
]
# Add motifs between the boundaries
motif_mask = np.logical_or(
in_bounds(
all_motif_coordinates,
domain_boundaries.max(),
parent_domain_boundaries.max(),
),
in_bounds(
all_motif_coordinates,
parent_domain_boundaries.min(),
domain_boundaries.min(),
),
)
# Append attributes to existing list
motif_index.extend(np.nonzero(motif_mask)[0])
motif_coordinates.extend(all_motif_coordinates[motif_mask])
motif_domain_index.extend(np.full(motif_mask.sum(), domain_idx))
return motif_index, motif_coordinates, motif_domain_index
# Use function to get attributes for promoters and DnaA boxes
TU_index, promoter_coordinates, promoter_domain_index = get_motif_attributes(
all_promoter_coordinates
)
cistron_index, gene_coordinates, gene_domain_index = get_motif_attributes(
all_gene_coordinates
)
_, DnaA_box_coordinates, DnaA_box_domain_index = get_motif_attributes(
all_DnaA_box_coordinates
)
# Add promoters as unique molecules and set attributes
# Note: the bound_TF attribute is properly initialized in the function
# initialize_transcription_factors
n_promoter = len(TU_index)
n_tf = len(sim_data.process.transcription_regulation.tf_ids)
unique_molecules["promoter"] = create_new_unique_molecules(
"promoter",
n_promoter,
sim_data,
unique_id_rng,
domain_index=promoter_domain_index,
coordinates=promoter_coordinates,
TU_index=TU_index,
bound_TF=np.zeros((n_promoter, n_tf), dtype=bool),
)
# Add genes as unique molecules and set attributes
n_gene = len(cistron_index)
unique_molecules["gene"] = create_new_unique_molecules(
"gene",
n_gene,
sim_data,
unique_id_rng,
cistron_index=cistron_index,
coordinates=gene_coordinates,
domain_index=gene_domain_index,
)
# Add DnaA boxes as unique molecules and set attributes
n_DnaA_box = len(DnaA_box_coordinates)
unique_molecules["DnaA_box"] = create_new_unique_molecules(
"DnaA_box",
n_DnaA_box,
sim_data,
unique_id_rng,
domain_index=DnaA_box_domain_index,
coordinates=DnaA_box_coordinates,
DnaA_bound=np.zeros(n_DnaA_box, dtype=bool),
)
[docs]
def initialize_transcription_factors(
bulk_state, unique_molecules, sim_data, random_state
):
"""
Initialize transcription factors that are bound to the chromosome. For each
type of transcription factor, this function calculates the total number of
transcription factors that should be bound to the chromosome using the
binding probabilities of each transcription factor and the number of
available promoter sites. The calculated number of transcription factors
are then distributed randomly to promoters, whose bound_TF attributes and
submasses are updated correspondingly.
"""
# Get transcription factor properties from sim_data
tf_ids = sim_data.process.transcription_regulation.tf_ids
tf_to_tf_type = sim_data.process.transcription_regulation.tf_to_tf_type
p_promoter_bound_TF = sim_data.process.transcription_regulation.p_promoter_bound_tf
# Build dict that maps TFs to transcription units they regulate
delta_prob = sim_data.process.transcription_regulation.delta_prob
TF_to_TU_idx = {}
for i, tf in enumerate(tf_ids):
TF_to_TU_idx[tf] = delta_prob["deltaI"][delta_prob["deltaJ"] == i]
# Get views into bulk molecule representations of transcription factors
active_tf_view = {}
inactive_tf_view = {}
active_tf_view_idx = {}
inactive_tf_view_idx = {}
for tf in tf_ids:
tf_idx = bulk_name_to_idx(tf + "[c]", bulk_state["id"])
active_tf_view[tf] = bulk_state["count"][tf_idx]
active_tf_view_idx[tf] = tf_idx
if tf_to_tf_type[tf] == "1CS":
if tf == sim_data.process.transcription_regulation.active_to_bound[tf]:
inactive_tf_idx = bulk_name_to_idx(
sim_data.process.equilibrium.get_unbound(tf + "[c]"),
bulk_state["id"],
)
inactive_tf_view[tf] = bulk_state["count"][inactive_tf_idx]
else:
inactive_tf_idx = bulk_name_to_idx(
sim_data.process.transcription_regulation.active_to_bound[tf]
+ "[c]",
bulk_state["id"],
)
inactive_tf_view[tf] = bulk_state["count"][inactive_tf_idx]
elif tf_to_tf_type[tf] == "2CS":
inactive_tf_idx = bulk_name_to_idx(
sim_data.process.two_component_system.active_to_inactive_tf[tf + "[c]"],
bulk_state["id"],
)
inactive_tf_view[tf] = bulk_state["count"][inactive_tf_idx]
inactive_tf_view_idx[tf] = inactive_tf_idx
# Get masses of active transcription factors
tf_indexes = [np.where(bulk_state["id"] == tf_id + "[c]")[0][0] for tf_id in tf_ids]
active_tf_masses = (
sim_data.internal_state.bulk_molecules.bulk_data["mass"][tf_indexes]
/ sim_data.constants.n_avogadro
).asNumber(units.fg)
# Get TU indices of promoters
TU_index = unique_molecules["promoter"]["TU_index"]
# Initialize bound_TF array
bound_TF = np.zeros((len(TU_index), len(tf_ids)), dtype=bool)
for tf_idx, tf_id in enumerate(tf_ids):
# Get counts of transcription factors
active_tf_counts = active_tf_view[tf_id]
# If there are no active transcription factors at initialization,
# continue to the next transcription factor
if active_tf_counts == 0:
continue
# Compute probability of binding the promoter
if tf_to_tf_type[tf_id] == "0CS":
p_promoter_bound = 1.0
else:
inactive_tf_counts = inactive_tf_view[tf_id]
p_promoter_bound = p_promoter_bound_TF(active_tf_counts, inactive_tf_counts)
# Determine the number of available promoter sites
available_promoters = np.isin(TU_index, TF_to_TU_idx[tf_id])
n_available_promoters = available_promoters.sum()
# Calculate the number of promoters that should be bound
n_to_bind = int(
stochasticRound(
random_state, np.full(n_available_promoters, p_promoter_bound)
).sum()
)
bound_locs = np.zeros(n_available_promoters, dtype=bool)
if n_to_bind > 0:
# Determine randomly which DNA targets to bind based on which of
# the following is more limiting:
# number of promoter sites to bind, or number of active
# transcription factors
bound_locs[
random_state.choice(
n_available_promoters,
size=min(n_to_bind, active_tf_view[tf_id]),
replace=False,
)
] = True
# Update count of free transcription factors
bulk_state["count"][active_tf_view_idx[tf_id]] -= bound_locs.sum()
# Update bound_TF array
bound_TF[available_promoters, tf_idx] = bound_locs
# Calculate masses of bound TFs
mass_diffs = bound_TF.dot(active_tf_masses)
# Reset bound_TF attribute of promoters
unique_molecules["promoter"]["bound_TF"] = bound_TF
# Add mass_diffs array to promoter submass
for submass, i in sim_data.submass_name_to_index.items():
unique_molecules["promoter"][f"massDiff_{submass}"] = mass_diffs[:, i]
[docs]
def initialize_transcription(
bulk_state,
unique_molecules,
sim_data,
random_state,
unique_id_rng,
ppgpp_regulation,
trna_attenuation,
):
"""
Activate RNA polymerases as unique molecules, and distribute them along
lengths of trancription units, while decreasing counts of unactivated RNA
polymerases (APORNAP-CPLX[c]). Also initialize unique molecule
representations of fully transcribed mRNAs and partially transcribed RNAs,
using counts of mRNAs initialized as bulk molecules, and the attributes of
initialized RNA polymerases. The counts of full mRNAs represented as bulk
molecules are reset to zero.
RNA polymerases are placed randomly across the length of each transcription
unit, with the synthesis probabilities for each TU determining the number of
RNA polymerases placed at each gene.
"""
# Load parameters
rna_lengths = sim_data.process.transcription.rna_data["length"].asNumber()
rna_masses = (
sim_data.process.transcription.rna_data["mw"] / sim_data.constants.n_avogadro
).asNumber(units.fg)
current_media_id = sim_data.conditions[sim_data.condition]["nutrients"]
frac_active_rnap = sim_data.process.transcription.rnapFractionActiveDict[
current_media_id
]
inactive_rnap_idx = bulk_name_to_idx(
sim_data.molecule_ids.full_RNAP, bulk_state["id"]
)
inactive_RNAP_counts = bulk_state["count"][inactive_rnap_idx]
rna_sequences = sim_data.process.transcription.transcription_sequences
nt_weights = sim_data.process.transcription.transcription_monomer_weights
end_weight = sim_data.process.transcription.transcription_end_weight
replichore_lengths = sim_data.process.replication.replichore_lengths
chromosome_length = replichore_lengths.sum()
# Number of rnaPoly to activate
n_RNAPs_to_activate = np.int64(frac_active_rnap * inactive_RNAP_counts)
# Get attributes of promoters
TU_index, bound_TF, domain_index_promoters = attrs(
unique_molecules["promoter"], ["TU_index", "bound_TF", "domain_index"]
)
# Parameters for rnaSynthProb
if ppgpp_regulation:
doubling_time = sim_data.condition_to_doubling_time[sim_data.condition]
ppgpp_conc = sim_data.growth_rate_parameters.get_ppGpp_conc(doubling_time)
basal_prob, _ = sim_data.process.transcription.synth_prob_from_ppgpp(
ppgpp_conc, sim_data.process.replication.get_average_copy_number
)
ppgpp_scale = basal_prob[TU_index]
# Use original delta prob if no ppGpp basal prob
ppgpp_scale[ppgpp_scale == 0] = 1
else:
basal_prob = sim_data.process.transcription_regulation.basal_prob.copy()
ppgpp_scale = 1
if trna_attenuation:
basal_prob[sim_data.process.transcription.attenuated_rna_indices] += (
sim_data.process.transcription.attenuation_basal_prob_adjustments
)
n_TUs = len(basal_prob)
delta_prob_matrix = sim_data.process.transcription_regulation.get_delta_prob_matrix(
dense=True, ppgpp=ppgpp_regulation
)
# Synthesis probabilities for different categories of genes
rna_synth_prob_fractions = sim_data.process.transcription.rnaSynthProbFraction
rna_synth_prob_R_protein = sim_data.process.transcription.rnaSynthProbRProtein
rna_synth_prob_rna_polymerase = (
sim_data.process.transcription.rnaSynthProbRnaPolymerase
)
# Get coordinates and transcription directions of transcription units
replication_coordinate = sim_data.process.transcription.rna_data[
"replication_coordinate"
]
transcription_direction = sim_data.process.transcription.rna_data["is_forward"]
# Determine changes from genetic perturbations
genetic_perturbations = {}
perturbations = getattr(sim_data, "genetic_perturbations", {})
if len(perturbations) > 0:
probability_indexes = [
(index, sim_data.genetic_perturbations[rna_data["id"]])
for index, rna_data in enumerate(sim_data.process.transcription.rna_data)
if rna_data["id"] in sim_data.genetic_perturbations
]
genetic_perturbations = {
"fixedRnaIdxs": [pair[0] for pair in probability_indexes],
"fixedSynthProbs": [pair[1] for pair in probability_indexes],
}
# ID Groups
idx_rRNA = np.where(sim_data.process.transcription.rna_data["is_rRNA"])[0]
idx_mRNA = np.where(sim_data.process.transcription.rna_data["is_mRNA"])[0]
idx_tRNA = np.where(sim_data.process.transcription.rna_data["is_tRNA"])[0]
idx_rprotein = np.where(
sim_data.process.transcription.rna_data["includes_ribosomal_protein"]
)[0]
idx_rnap = np.where(sim_data.process.transcription.rna_data["includes_RNAP"])[0]
# Calculate probabilities of the RNAP binding to the promoters
promoter_init_probs = basal_prob[TU_index] + ppgpp_scale * np.multiply(
delta_prob_matrix[TU_index, :], bound_TF
).sum(axis=1)
if len(genetic_perturbations) > 0:
rescale_initiation_probs(
promoter_init_probs,
TU_index,
genetic_perturbations["fixedSynthProbs"],
genetic_perturbations["fixedRnaIdxs"],
)
# Adjust probabilities to not be negative
promoter_init_probs[promoter_init_probs < 0] = 0.0
promoter_init_probs /= promoter_init_probs.sum()
if np.any(promoter_init_probs < 0):
raise Exception("Have negative RNA synthesis probabilities")
# Adjust synthesis probabilities depending on environment
synth_prob_fractions = rna_synth_prob_fractions[current_media_id]
# Create masks for different types of RNAs
is_mRNA = np.isin(TU_index, idx_mRNA)
is_tRNA = np.isin(TU_index, idx_tRNA)
is_rRNA = np.isin(TU_index, idx_rRNA)
is_rprotein = np.isin(TU_index, idx_rprotein)
is_rnap = np.isin(TU_index, idx_rnap)
is_fixed = is_tRNA | is_rRNA | is_rprotein | is_rnap
# Rescale initiation probabilities based on type of RNA
promoter_init_probs[is_mRNA] *= (
synth_prob_fractions["mRna"] / promoter_init_probs[is_mRNA].sum()
)
promoter_init_probs[is_tRNA] *= (
synth_prob_fractions["tRna"] / promoter_init_probs[is_tRNA].sum()
)
promoter_init_probs[is_rRNA] *= (
synth_prob_fractions["rRna"] / promoter_init_probs[is_rRNA].sum()
)
# Set fixed synthesis probabilities for RProteins and RNAPs
rescale_initiation_probs(
promoter_init_probs,
TU_index,
np.concatenate(
(
rna_synth_prob_R_protein[current_media_id],
rna_synth_prob_rna_polymerase[current_media_id],
)
),
np.concatenate((idx_rprotein, idx_rnap)),
)
assert promoter_init_probs[is_fixed].sum() < 1.0
# Adjust for attenuation that will stop transcription after initiation
if trna_attenuation:
attenuation_readthrough = {
idx: prob
for idx, prob in zip(
sim_data.process.transcription.attenuated_rna_indices,
sim_data.process.transcription.attenuation_readthrough[
sim_data.condition
],
)
}
readthrough_adjustment = np.array(
[attenuation_readthrough.get(idx, 1) for idx in TU_index]
)
promoter_init_probs *= readthrough_adjustment
scale_the_rest_by = (
1.0 - promoter_init_probs[is_fixed].sum()
) / promoter_init_probs[~is_fixed].sum()
promoter_init_probs[~is_fixed] *= scale_the_rest_by
# normalize to length of rna
init_prob_length_adjusted = promoter_init_probs * rna_lengths[TU_index]
init_prob_normalized = init_prob_length_adjusted / init_prob_length_adjusted.sum()
# Sample a multinomial distribution of synthesis probabilities to determine
# what RNA are initialized
n_initiations = random_state.multinomial(n_RNAPs_to_activate, init_prob_normalized)
# Build array of transcription unit indexes for partially transcribed mRNAs
# and domain indexes for RNAPs
TU_index_partial_RNAs = np.repeat(TU_index, n_initiations)
domain_index_rnap = np.repeat(domain_index_promoters, n_initiations)
# Build arrays of starting coordinates and transcription directions
starting_coordinates = replication_coordinate[TU_index_partial_RNAs]
is_forward = transcription_direction[TU_index_partial_RNAs]
# Randomly advance RNAPs along the transcription units
# TODO (Eran): make sure there aren't any RNAPs at same location on same TU
updated_lengths = np.array(
random_state.rand(n_RNAPs_to_activate) * rna_lengths[TU_index_partial_RNAs],
dtype=int,
)
# Rescale boolean array of directions to an array of 1's and -1's.
direction_rescaled = (2 * (is_forward - 0.5)).astype(np.int64)
# Compute the updated coordinates of RNAPs. Coordinates of RNAPs moving in
# the positive direction are increased, whereas coordinates of RNAPs moving
# in the negative direction are decreased.
updated_coordinates = starting_coordinates + np.multiply(
direction_rescaled, updated_lengths
)
# Reset coordinates of RNAPs that cross the boundaries between right and
# left replichores
updated_coordinates[updated_coordinates > replichore_lengths[0]] -= (
chromosome_length
)
updated_coordinates[updated_coordinates < -replichore_lengths[1]] += (
chromosome_length
)
# Update mass
sequences = rna_sequences[TU_index_partial_RNAs]
added_mass = computeMassIncrease(sequences, updated_lengths, nt_weights)
added_mass[updated_lengths != 0] += end_weight # add endWeight to all new Rna
# Masses of partial mRNAs are counted as mRNA mass as they are already
# functional, but the masses of other types of partial RNAs are counted as
# generic RNA mass.
added_RNA_mass = added_mass.copy()
added_mRNA_mass = added_mass.copy()
is_mRNA_partial_RNAs = np.isin(TU_index_partial_RNAs, idx_mRNA)
added_RNA_mass[is_mRNA_partial_RNAs] = 0
added_mRNA_mass[np.logical_not(is_mRNA_partial_RNAs)] = 0
# Add active RNAPs and get their unique indexes
unique_molecules["active_RNAP"] = create_new_unique_molecules(
"active_RNAP",
n_RNAPs_to_activate,
sim_data,
unique_id_rng,
domain_index=domain_index_rnap,
coordinates=updated_coordinates,
is_forward=is_forward,
)
# Decrement counts of bulk inactive RNAPs
rnap_idx = bulk_name_to_idx(sim_data.molecule_ids.full_RNAP, bulk_state["id"])
bulk_state["count"][rnap_idx] = inactive_RNAP_counts - n_RNAPs_to_activate
# Add partially transcribed RNAs
partial_rnas = create_new_unique_molecules(
"RNA",
n_RNAPs_to_activate,
sim_data,
unique_id_rng,
TU_index=TU_index_partial_RNAs,
transcript_length=updated_lengths,
is_mRNA=is_mRNA_partial_RNAs,
is_full_transcript=np.zeros(n_RNAPs_to_activate, dtype=bool),
can_translate=is_mRNA_partial_RNAs,
RNAP_index=unique_molecules["active_RNAP"]["unique_index"],
massDiff_nonspecific_RNA=added_RNA_mass,
massDiff_mRNA=added_mRNA_mass,
)
# Get counts of mRNAs initialized as bulk molecules
mRNA_ids = sim_data.process.transcription.rna_data["id"][
sim_data.process.transcription.rna_data["is_mRNA"]
]
mRNA_idx = bulk_name_to_idx(mRNA_ids, bulk_state["id"])
mRNA_counts = bulk_state["count"][mRNA_idx]
# Subtract number of partially transcribed mRNAs that were initialized.
# Note: some mRNAs with high degradation rates have more partial mRNAs than
# the expected total number of mRNAs - for these mRNAs we simply set the
# initial full mRNA counts to be zero.
partial_mRNA_counts = np.bincount(
TU_index_partial_RNAs[is_mRNA_partial_RNAs], minlength=n_TUs
)[idx_mRNA]
full_mRNA_counts = (mRNA_counts - partial_mRNA_counts).clip(min=0)
# Get array of TU indexes for each full mRNA
TU_index_full_mRNAs = np.repeat(idx_mRNA, full_mRNA_counts)
# Add fully transcribed mRNAs. The RNAP_index attribute of these molecules
# are set to -1.
full_rnas = create_new_unique_molecules(
"RNA",
len(TU_index_full_mRNAs),
sim_data,
unique_id_rng,
TU_index=TU_index_full_mRNAs,
transcript_length=rna_lengths[TU_index_full_mRNAs],
is_mRNA=np.ones_like(TU_index_full_mRNAs, dtype=bool),
is_full_transcript=np.ones_like(TU_index_full_mRNAs, dtype=bool),
can_translate=np.ones_like(TU_index_full_mRNAs, dtype=bool),
RNAP_index=np.full(TU_index_full_mRNAs.shape, -1, dtype=np.int64),
massDiff_mRNA=rna_masses[TU_index_full_mRNAs],
)
unique_molecules["RNA"] = np.concatenate((partial_rnas, full_rnas))
# Reset counts of bulk mRNAs to zero
bulk_state["count"][mRNA_idx] = 0
[docs]
def initialize_chromosomal_segments(unique_molecules, sim_data, unique_id_rng):
"""
Initialize unique molecule representations of chromosomal segments. All
chromosomal segments are assumed to be at their relaxed states upon
initialization.
"""
# Load parameters
relaxed_DNA_base_pairs_per_turn = (
sim_data.process.chromosome_structure.relaxed_DNA_base_pairs_per_turn
)
terC_index = sim_data.process.chromosome_structure.terC_dummy_molecule_index
replichore_lengths = sim_data.process.replication.replichore_lengths
min_coordinates = -replichore_lengths[1]
max_coordinates = replichore_lengths[0]
# Get attributes of replisomes, active RNAPs, chromosome domains, full
# chromosomes, and oriCs
(replisome_coordinates, replisome_domain_indexes, replisome_unique_indexes) = attrs(
unique_molecules["active_replisome"],
["coordinates", "domain_index", "unique_index"],
)
(
active_RNAP_coordinates,
active_RNAP_domain_indexes,
active_RNAP_unique_indexes,
) = attrs(
unique_molecules["active_RNAP"], ["coordinates", "domain_index", "unique_index"]
)
chromosome_domain_domain_indexes, child_domains = attrs(
unique_molecules["chromosome_domain"], ["domain_index", "child_domains"]
)
(full_chromosome_domain_indexes,) = attrs(
unique_molecules["full_chromosome"], ["domain_index"]
)
(origin_domain_indexes,) = attrs(unique_molecules["oriC"], ["domain_index"])
# Initialize chromosomal segment attributes
all_boundary_molecule_indexes = np.empty((0, 2), dtype=np.int64)
all_boundary_coordinates = np.empty((0, 2), dtype=np.int64)
all_segment_domain_indexes = np.array([], dtype=np.int32)
all_linking_numbers = np.array([], dtype=np.float64)
def get_chromosomal_segment_attributes(
coordinates, unique_indexes, spans_oriC, spans_terC
):
"""
Returns the attributes of all chromosomal segments from a continuous
stretch of DNA, given the coordinates and unique indexes of all
boundary molecules.
"""
coordinates_argsort = np.argsort(coordinates)
coordinates_sorted = coordinates[coordinates_argsort]
unique_indexes_sorted = unique_indexes[coordinates_argsort]
# Add dummy molecule at terC if domain spans terC
if spans_terC:
coordinates_sorted = np.insert(
coordinates_sorted,
[0, len(coordinates_sorted)],
[min_coordinates, max_coordinates],
)
unique_indexes_sorted = np.insert(
unique_indexes_sorted, [0, len(unique_indexes_sorted)], terC_index
)
boundary_molecule_indexes = np.hstack(
(
unique_indexes_sorted[:-1][:, np.newaxis],
unique_indexes_sorted[1:][:, np.newaxis],
)
)
boundary_coordinates = np.hstack(
(
coordinates_sorted[:-1][:, np.newaxis],
coordinates_sorted[1:][:, np.newaxis],
)
)
# Remove segment that spans oriC if the domain does not span oriC
if not spans_oriC:
oriC_segment_index = np.where(
np.sign(boundary_coordinates).sum(axis=1) == 0
)[0]
assert len(oriC_segment_index) == 1
boundary_molecule_indexes = np.delete(
boundary_molecule_indexes, oriC_segment_index, 0
)
boundary_coordinates = np.delete(
boundary_coordinates, oriC_segment_index, 0
)
# Assumes all segments are at their relaxed state at initialization
linking_numbers = (
boundary_coordinates[:, 1] - boundary_coordinates[:, 0]
) / relaxed_DNA_base_pairs_per_turn
return boundary_molecule_indexes, boundary_coordinates, linking_numbers
# Loop through each domain index
for domain_index in chromosome_domain_domain_indexes:
domain_spans_oriC = domain_index in origin_domain_indexes
domain_spans_terC = domain_index in full_chromosome_domain_indexes
# Get coordinates and indexes of all RNAPs on this domain
RNAP_domain_mask = active_RNAP_domain_indexes == domain_index
molecule_coordinates_this_domain = active_RNAP_coordinates[RNAP_domain_mask]
molecule_indexes_this_domain = active_RNAP_unique_indexes[RNAP_domain_mask]
# Append coordinates and indexes of replisomes on this domain, if any
if not domain_spans_oriC:
replisome_domain_mask = replisome_domain_indexes == domain_index
molecule_coordinates_this_domain = np.concatenate(
(
molecule_coordinates_this_domain,
replisome_coordinates[replisome_domain_mask],
)
)
molecule_indexes_this_domain = np.concatenate(
(
molecule_indexes_this_domain,
replisome_unique_indexes[replisome_domain_mask],
)
)
# Append coordinates and indexes of parent domain replisomes, if any
if not domain_spans_terC:
parent_domain_index = chromosome_domain_domain_indexes[
np.where(child_domains == domain_index)[0][0]
]
replisome_parent_domain_mask = (
replisome_domain_indexes == parent_domain_index
)
molecule_coordinates_this_domain = np.concatenate(
(
molecule_coordinates_this_domain,
replisome_coordinates[replisome_parent_domain_mask],
)
)
molecule_indexes_this_domain = np.concatenate(
(
molecule_indexes_this_domain,
replisome_unique_indexes[replisome_parent_domain_mask],
)
)
# Get attributes of chromosomal segments on this domain
(
boundary_molecule_indexes_this_domain,
boundary_coordinates_this_domain,
linking_numbers_this_domain,
) = get_chromosomal_segment_attributes(
molecule_coordinates_this_domain,
molecule_indexes_this_domain,
domain_spans_oriC,
domain_spans_terC,
)
# Append to existing array of attributes
all_boundary_molecule_indexes = np.vstack(
[all_boundary_molecule_indexes, boundary_molecule_indexes_this_domain]
)
all_boundary_coordinates = np.vstack(
(all_boundary_coordinates, boundary_coordinates_this_domain)
)
all_segment_domain_indexes = np.concatenate(
(
all_segment_domain_indexes,
np.full(len(linking_numbers_this_domain), domain_index, dtype=np.int32),
)
)
all_linking_numbers = np.concatenate(
(all_linking_numbers, linking_numbers_this_domain)
)
# Confirm total counts of all segments
n_segments = len(all_linking_numbers)
assert (
n_segments
== len(active_RNAP_unique_indexes) + 1.5 * len(replisome_unique_indexes) + 1
)
# Add chromosomal segments
unique_molecules["chromosomal_segment"] = create_new_unique_molecules(
"chromosomal_segment",
n_segments,
sim_data,
unique_id_rng,
boundary_molecule_indexes=all_boundary_molecule_indexes,
boundary_coordinates=all_boundary_coordinates,
domain_index=all_segment_domain_indexes,
linking_number=all_linking_numbers,
)
[docs]
def initialize_translation(
bulk_state, unique_molecules, sim_data, random_state, unique_id_rng
):
"""
Activate ribosomes as unique molecules, and distribute them along lengths
of mRNAs, while decreasing counts of unactivated ribosomal subunits (30S
and 50S).
Ribosomes are placed randomly across the lengths of each mRNA.
"""
# Load translation parameters
current_nutrients = sim_data.conditions[sim_data.condition]["nutrients"]
frac_active_ribosome = sim_data.process.translation.ribosomeFractionActiveDict[
current_nutrients
]
protein_sequences = sim_data.process.translation.translation_sequences
protein_lengths = sim_data.process.translation.monomer_data["length"].asNumber()
translation_efficiencies = normalize(
sim_data.process.translation.translation_efficiencies_by_monomer
)
aa_weights_incorporated = sim_data.process.translation.translation_monomer_weights
end_weight = sim_data.process.translation.translation_end_weight
cistron_lengths = sim_data.process.transcription.cistron_data["length"].asNumber(
units.nt
)
TU_ids = sim_data.process.transcription.rna_data["id"]
monomer_index_to_tu_indexes = sim_data.relation.monomer_index_to_tu_indexes
monomer_index_to_cistron_index = {
i: sim_data.process.transcription._cistron_id_to_index[monomer["cistron_id"]]
for (i, monomer) in enumerate(sim_data.process.translation.monomer_data)
}
# Get attributes of RNAs
(
TU_index_all_RNAs,
length_all_RNAs,
is_mRNA,
is_full_transcript_all_RNAs,
unique_index_all_RNAs,
) = attrs(
unique_molecules["RNA"],
[
"TU_index",
"transcript_length",
"is_mRNA",
"is_full_transcript",
"unique_index",
],
)
TU_index_mRNAs = TU_index_all_RNAs[is_mRNA]
length_mRNAs = length_all_RNAs[is_mRNA]
is_full_transcript_mRNAs = is_full_transcript_all_RNAs[is_mRNA]
unique_index_mRNAs = unique_index_all_RNAs[is_mRNA]
# Calculate available template lengths of each mRNA cistron from fully
# transcribed mRNA transcription units
TU_index_full_mRNAs = TU_index_mRNAs[is_full_transcript_mRNAs]
TU_counts_full_mRNAs = np.bincount(TU_index_full_mRNAs, minlength=len(TU_ids))
cistron_counts_full_mRNAs = (
sim_data.process.transcription.cistron_tu_mapping_matrix.dot(
TU_counts_full_mRNAs
)
)
available_cistron_lengths = np.multiply(cistron_counts_full_mRNAs, cistron_lengths)
# Add available template lengths from each partially transcribed mRNAs
TU_index_incomplete_mRNAs = TU_index_mRNAs[np.logical_not(is_full_transcript_mRNAs)]
length_incomplete_mRNAs = length_mRNAs[np.logical_not(is_full_transcript_mRNAs)]
TU_index_to_mRNA_lengths = {}
for TU_index, length in zip(TU_index_incomplete_mRNAs, length_incomplete_mRNAs):
TU_index_to_mRNA_lengths.setdefault(TU_index, []).append(length)
for TU_index, available_lengths in TU_index_to_mRNA_lengths.items():
cistron_indexes = sim_data.process.transcription.rna_id_to_cistron_indexes(
TU_ids[TU_index]
)
cistron_start_positions = np.array(
[
sim_data.process.transcription.cistron_start_end_pos_in_tu[
(cistron_index, TU_index)
][0]
for cistron_index in cistron_indexes
]
)
for length in available_lengths:
available_cistron_lengths[cistron_indexes] += np.clip(
length - cistron_start_positions, 0, cistron_lengths[cistron_indexes]
)
# Find number of ribosomes to activate
ribosome30S_idx = bulk_name_to_idx(
sim_data.molecule_ids.s30_full_complex, bulk_state["id"]
)
ribosome30S = bulk_state["count"][ribosome30S_idx]
ribosome50S_idx = bulk_name_to_idx(
sim_data.molecule_ids.s50_full_complex, bulk_state["id"]
)
ribosome50S = bulk_state["count"][ribosome50S_idx]
inactive_ribosome_count = np.minimum(ribosome30S, ribosome50S)
n_ribosomes_to_activate = np.int64(frac_active_ribosome * inactive_ribosome_count)
# Add total available template lengths as weights and normalize
protein_init_probs = normalize(
available_cistron_lengths[sim_data.relation.cistron_to_monomer_mapping]
* translation_efficiencies
)
# Sample a multinomial distribution of synthesis probabilities to determine
# which types of mRNAs are initialized
n_new_proteins = random_state.multinomial(
n_ribosomes_to_activate, protein_init_probs
)
# Build attributes for active ribosomes
protein_indexes = np.empty(n_ribosomes_to_activate, np.int64)
cistron_start_positions_on_mRNA = np.empty(n_ribosomes_to_activate, np.int64)
positions_on_mRNA_from_cistron_start_site = np.empty(
n_ribosomes_to_activate, np.int64
)
mRNA_indexes = np.empty(n_ribosomes_to_activate, np.int64)
start_index = 0
nonzero_count = n_new_proteins > 0
for protein_index, protein_counts in zip(
np.arange(n_new_proteins.size)[nonzero_count], n_new_proteins[nonzero_count]
):
# Set protein index
protein_indexes[start_index : start_index + protein_counts] = protein_index
# Get index of cistron corresponding to this protein
cistron_index = monomer_index_to_cistron_index[protein_index]
# Initialize list of available lengths for each transcript and the
# indexes of each transcript in the list of mRNA attributes
available_lengths = []
attribute_indexes = []
cistron_start_positions = []
# Distribute ribosomes among mRNAs that produce this protein, weighted
# by their lengths
for TU_index in monomer_index_to_tu_indexes[protein_index]:
attribute_indexes_this_TU = np.where(TU_index_mRNAs == TU_index)[0]
cistron_start_position = (
sim_data.process.transcription.cistron_start_end_pos_in_tu[
(cistron_index, TU_index)
][0]
)
available_lengths.extend(
np.clip(
length_mRNAs[attribute_indexes_this_TU] - cistron_start_position,
0,
cistron_lengths[cistron_index],
)
)
attribute_indexes.extend(attribute_indexes_this_TU)
cistron_start_positions.extend(
[cistron_start_position] * len(attribute_indexes_this_TU)
)
available_lengths = np.array(available_lengths)
attribute_indexes = np.array(attribute_indexes)
cistron_start_positions = np.array(cistron_start_positions)
n_ribosomes_per_RNA = random_state.multinomial(
protein_counts, normalize(available_lengths)
)
# Get unique indexes of each mRNA
mRNA_indexes[start_index : start_index + protein_counts] = np.repeat(
unique_index_mRNAs[attribute_indexes], n_ribosomes_per_RNA
)
# Get full length of this polypeptide
peptide_full_length = protein_lengths[protein_index]
# Randomly place ribosomes along the length of each mRNA, capped by the
# mRNA length expected from the full polypeptide length to prevent
# ribosomes from overshooting full peptide lengths
cistron_start_positions_on_mRNA[start_index : start_index + protein_counts] = (
np.repeat(cistron_start_positions, n_ribosomes_per_RNA)
)
positions_on_mRNA_from_cistron_start_site[
start_index : start_index + protein_counts
] = np.floor(
random_state.rand(protein_counts)
* np.repeat(
np.minimum(available_lengths, peptide_full_length * 3),
n_ribosomes_per_RNA,
)
)
start_index += protein_counts
# Calculate the lengths of the partial polypeptide, and rescale position on
# mRNA to be a multiple of three using this peptide length
peptide_lengths = np.floor_divide(positions_on_mRNA_from_cistron_start_site, 3)
positions_on_mRNA = cistron_start_positions_on_mRNA + 3 * peptide_lengths
# Update masses of partially translated proteins
sequences = protein_sequences[protein_indexes]
mass_increase_protein = computeMassIncrease(
sequences, peptide_lengths, aa_weights_incorporated
)
# Add end weight
mass_increase_protein[peptide_lengths != 0] += end_weight
# Add active ribosomes
unique_molecules["active_ribosome"] = create_new_unique_molecules(
"active_ribosome",
n_ribosomes_to_activate,
sim_data,
unique_id_rng,
protein_index=protein_indexes,
peptide_length=peptide_lengths,
mRNA_index=mRNA_indexes,
pos_on_mRNA=positions_on_mRNA,
massDiff_protein=mass_increase_protein,
)
# Decrease counts of free 30S and 50S ribosomal subunits
bulk_state["count"][ribosome30S_idx] = ribosome30S - n_ribosomes_to_activate
bulk_state["count"][ribosome50S_idx] = ribosome50S - n_ribosomes_to_activate
[docs]
def determine_chromosome_state(
tau: Unum,
replichore_length: Unum,
n_max_replisomes: int,
place_holder: int,
cell_mass: Unum,
critical_mass: Unum,
replication_rate: float,
) -> tuple[
dict[str, npt.NDArray[np.int32]],
dict[str, npt.NDArray[Any]],
dict[str, npt.NDArray[np.int32]],
]:
"""
Calculates the attributes of oriC's, replisomes, and chromosome domains on
the chromosomes at the beginning of the cell cycle.
Args:
tau: the doubling time of the cell (with Unum time unit)
replichore_length: the amount of DNA to be replicated per fork, usually
half of the genome, in base-pairs (with Unum nucleotide unit)
n_max_replisomes: the maximum number of replisomes that can be formed
given the initial counts of replisome subunits
place_holder: placeholder value for chromosome domains without child
domains
cell_mass: total mass of the cell with mass units (with Unum mass unit)
critical_mass: mass per oriC before replication is initiated
(with Unum mass unit)
replication_rate: rate of nucleotide elongation
(with Unum nucleotides per time unit)
Returns:
Three dictionaries, each containing updates to attributes of a unique molecule type.
- ``oric_state``: dictionary of the following format::
{'domain_index': a vector of integers indicating which chromosome domain the
oriC sequence belongs to.}
- ``replisome_state``: dictionary of the following format::
{'coordinates': a vector of integers that indicates where the replisomes
are located on the chromosome relative to the origin in base pairs,
'right_replichore': a vector of boolean values that indicates whether the
replisome is on the right replichore (True) or the left replichore (False),
'domain_index': a vector of integers indicating which chromosome domain the
replisomes belong to. The index of the "mother" domain of the replication
fork is assigned to the replisome}
- ``domain_state``: dictionary of the following format::
{'domain_index': the indexes of the domains,
'child_domains': the (n_domain X 2) array of the domain indexes of the two
children domains that are connected on the oriC side with the given domain.}
"""
# All inputs must be positive numbers
unitless_tau = tau.asNumber(units.s)
unitless_replichore_length = replichore_length.asNumber(units.nt)
assert unitless_tau >= 0, "tau value can't be negative."
assert unitless_replichore_length > 0, "replichore_length must be positive."
# Convert to unitless
unitless_cell_mass = cell_mass.asNumber(units.fg)
unitless_critical_mass = critical_mass.asNumber(units.fg)
# Calculate the maximum number of replication rounds given the maximum
# count of replisomes
n_max_rounds = int(np.log2(n_max_replisomes / 2 + 1))
# Calculate the number of active replication rounds
n_rounds = min(
n_max_rounds,
max(0, int(np.ceil(np.log2(unitless_cell_mass / unitless_critical_mass)))),
)
# Initialize arrays for replisomes
n_replisomes = 2 * (2**n_rounds - 1)
coordinates = np.zeros(n_replisomes, dtype=np.int64)
right_replichore_replisome = np.zeros(n_replisomes, dtype=bool)
domain_index_replisome = np.zeros(n_replisomes, dtype=np.int32)
# Initialize child domain array for chromosome domains
n_domains = 2 ** (n_rounds + 1) - 1
child_domains = np.full((n_domains, 2), place_holder, dtype=np.int32)
# Set domain_index attribute of oriC's and chromosome domains
domain_index_oric = np.arange(
2**n_rounds - 1, 2 ** (n_rounds + 1) - 1, dtype=np.int32
)
domain_index_domains = np.arange(0, n_domains, dtype=np.int32)
def n_events_before_this_round(round_idx):
"""
Calculates the number of replication events that happen before the
replication round index given as an argument. Since 2**i events happen
at each round i = 0, 1, ..., the sum of the number of events before
round j is 2**j - 1.
"""
return 2**round_idx - 1
# Loop through active replication rounds, starting from the oldest round.
# If n_round = 0 skip loop entirely - no active replication round.
for round_idx in np.arange(n_rounds):
# Determine at which location (base) of the chromosome the replication
# forks should be initialized to
round_critical_mass = 2**round_idx * unitless_critical_mass
growth_rate = np.log(2) / unitless_tau
replication_time = (
np.log(unitless_cell_mass / round_critical_mass) / growth_rate
)
# TODO: this should handle completed replication (instead of taking min)
# for accuracy but will likely never start with multiple chromosomes
fork_location = min(
np.floor(replication_time * replication_rate),
unitless_replichore_length - 1,
)
# Add 2^n initiation events per round. A single initiation event
# generates two replication forks.
n_events_this_round = 2**round_idx
# Set attributes of replisomes for this replication round
coordinates[
2 * n_events_before_this_round(round_idx) : 2
* n_events_before_this_round(round_idx + 1)
] = np.tile(np.array([fork_location, -fork_location]), n_events_this_round)
right_replichore_replisome[
2 * n_events_before_this_round(round_idx) : 2
* n_events_before_this_round(round_idx + 1)
] = np.tile(np.array([True, False]), n_events_this_round)
for i, domain_index in enumerate(
np.arange(
n_events_before_this_round(round_idx),
n_events_before_this_round(round_idx + 1),
)
):
domain_index_replisome[
2 * n_events_before_this_round(round_idx) + 2 * i : 2
* n_events_before_this_round(round_idx)
+ 2 * (i + 1)
] = np.repeat(domain_index, 2)
# Set attributes of chromosome domains for this replication round
for i, domain_index in enumerate(
np.arange(
n_events_before_this_round(round_idx + 1),
n_events_before_this_round(round_idx + 2),
2,
)
):
child_domains[n_events_before_this_round(round_idx) + i, :] = np.array(
[domain_index, domain_index + 1]
)
# Convert to numpy arrays and wrap into dictionaries
oric_state = {"domain_index": domain_index_oric}
replisome_state = {
"coordinates": coordinates,
"right_replichore": right_replichore_replisome,
"domain_index": domain_index_replisome,
}
domain_state = {
"child_domains": child_domains,
"domain_index": domain_index_domains,
}
return oric_state, replisome_state, domain_state
[docs]
def rescale_initiation_probs(init_probs, TU_index, fixed_synth_probs, fixed_TU_indexes):
"""
Rescales the initiation probabilities of each promoter such that the total
synthesis probabilities of certain types of RNAs are fixed to a
predetermined value. For instance, if there are two copies of promoters for
RNA A, whose synthesis probability should be fixed to 0.1, each promoter is
given an initiation probability of 0.05.
"""
for rna_idx, synth_prob in zip(fixed_TU_indexes, fixed_synth_probs):
fixed_rna_mask = TU_index == rna_idx
init_probs[fixed_rna_mask] = synth_prob / fixed_rna_mask.sum()
[docs]
def calculate_cell_mass(bulk_state, unique_molecules, sim_data):
"""
Calculates cell mass in femtograms.
"""
bulk_submass_names = [
f"{submass}_submass" for submass in sim_data.submass_name_to_index.keys()
]
cell_mass = (
bulk_state["count"]
.dot(rfn.structured_to_unstructured(bulk_state[bulk_submass_names]))
.sum()
)
if len(unique_molecules) > 0:
unique_masses = sim_data.internal_state.unique_molecule.unique_molecule_masses[
"mass"
].asNumber(units.fg / units.mol) / sim_data.constants.n_avogadro.asNumber(
1 / units.mol
)
unique_ids = sim_data.internal_state.unique_molecule.unique_molecule_masses[
"id"
]
unique_submass_names = [
f"massDiff_{submass}" for submass in sim_data.submass_name_to_index.keys()
]
for unique_id, unique_submasses in zip(unique_ids, unique_masses):
if unique_id in unique_molecules:
cell_mass += (
unique_molecules[unique_id]["_entryState"].sum() * unique_submasses
).sum()
cell_mass += rfn.structured_to_unstructured(
unique_molecules[unique_id][unique_submass_names]
).sum()
return units.fg * cell_mass
[docs]
def initialize_trna_charging(
bulk_state: np.ndarray,
unique_molecules: dict[str, np.ndarray],
sim_data: Any,
variable_elongation: bool,
):
"""
Initializes charged tRNA from uncharged tRNA and amino acids
Args:
bulk_state: Structured array with IDs and counts of all bulk molecules
unique_molecules: Mapping of unique molecule names to structured
arrays of their current simulation states
sim_data: Simulation data loaded from pickle generated by ParCa
variable_elongation: Sets max elongation higher if True
.. note::
Does not adjust for mass of amino acids on charged tRNA (~0.01% of cell mass)
"""
# Calculate cell volume for concentrations
cell_volume = (
calculate_cell_mass(bulk_state, unique_molecules, sim_data)
/ sim_data.constants.cell_density
)
counts_to_molar = 1 / (sim_data.constants.n_avogadro * cell_volume)
# Get molecule views and concentrations
transcription = sim_data.process.transcription
aa_from_synthetase = transcription.aa_from_synthetase
aa_from_trna = transcription.aa_from_trna
synthetases = counts(
bulk_state, bulk_name_to_idx(transcription.synthetase_names, bulk_state["id"])
)
uncharged_trna_idx = bulk_name_to_idx(
transcription.uncharged_trna_names, bulk_state["id"]
)
uncharged_trna = counts(bulk_state, uncharged_trna_idx)
charged_trna_idx = bulk_name_to_idx(
transcription.charged_trna_names, bulk_state["id"]
)
charged_trna = counts(bulk_state, charged_trna_idx)
aas = counts(
bulk_state,
bulk_name_to_idx(sim_data.molecule_groups.amino_acids, bulk_state["id"]),
)
ribosome_counts = unique_molecules["active_ribosome"]["_entryState"].sum()
synthetase_conc = counts_to_molar * np.dot(aa_from_synthetase, synthetases)
uncharged_trna_conc = counts_to_molar * np.dot(aa_from_trna, uncharged_trna)
charged_trna_conc = counts_to_molar * np.dot(aa_from_trna, charged_trna)
aa_conc = counts_to_molar * aas
ribosome_conc = counts_to_molar * ribosome_counts
# Estimate fraction of amino acids from sequences, excluding first index for padding of -1
_, aas_in_sequences = np.unique(
sim_data.process.translation.translation_sequences, return_counts=True
)
f = aas_in_sequences[1:] / np.sum(aas_in_sequences[1:])
# Estimate initial charging state
constants = sim_data.constants
transcription = sim_data.process.transcription
metabolism = sim_data.process.metabolism
elongation_max = (
constants.ribosome_elongation_rate_max
if variable_elongation
else constants.ribosome_elongation_rate_basal
)
charging_params = {
"kS": constants.synthetase_charging_rate.asNumber(1 / units.s),
"KMaa": transcription.aa_kms.asNumber(MICROMOLAR_UNITS),
"KMtf": transcription.trna_kms.asNumber(MICROMOLAR_UNITS),
"krta": constants.Kdissociation_charged_trna_ribosome.asNumber(
MICROMOLAR_UNITS
),
"krtf": constants.Kdissociation_uncharged_trna_ribosome.asNumber(
MICROMOLAR_UNITS
),
"max_elong_rate": float(elongation_max.asNumber(units.aa / units.s)),
"charging_mask": np.array(
[
aa not in REMOVED_FROM_CHARGING
for aa in sim_data.molecule_groups.amino_acids
]
),
"unit_conversion": metabolism.get_amino_acid_conc_conversion(MICROMOLAR_UNITS),
}
fraction_charged, *_ = calculate_trna_charging(
synthetase_conc,
uncharged_trna_conc,
charged_trna_conc,
aa_conc,
ribosome_conc,
f,
charging_params,
)
# Update counts of tRNA to match charging
total_trna_counts = uncharged_trna + charged_trna
charged_trna_counts = np.round(
total_trna_counts * np.dot(fraction_charged, aa_from_trna)
)
uncharged_trna_counts = total_trna_counts - charged_trna_counts
bulk_state["count"][charged_trna_idx] = charged_trna_counts
bulk_state["count"][uncharged_trna_idx] = uncharged_trna_counts