From 96e1ed89243fa76ae6a2f82b645afb96095b8d2d Mon Sep 17 00:00:00 2001 From: Sean Cheah Date: Sun, 25 Jan 2026 23:01:38 -0800 Subject: [PATCH 1/5] Test all combos of 5 boolean translation flags --- ecoli/composites/ecoli_master_tests.py | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/ecoli/composites/ecoli_master_tests.py b/ecoli/composites/ecoli_master_tests.py index 573a1a2b0..975d2bc7d 100644 --- a/ecoli/composites/ecoli_master_tests.py +++ b/ecoli/composites/ecoli_master_tests.py @@ -5,6 +5,8 @@ """ import os +from itertools import product + import numpy as np import pytest import warnings @@ -24,6 +26,40 @@ ) from ecoli.experiments.ecoli_master_sim import EcoliSim, CONFIG_DIR_PATH +TRANSLATION_SUPPLY_FLAGS = [ + "mechanistic_translation_supply", + "trna_charging", + "mechanistic_aa_transport", + "aa_supply_in_charging", + "translation_supply", +] +_FLAG_STATE_TUPLES = list(product([False, True], repeat=len(TRANSLATION_SUPPLY_FLAGS))) +TRANSLATION_FLAG_COMBINATIONS = [ + dict(zip(TRANSLATION_SUPPLY_FLAGS, combo)) for combo in _FLAG_STATE_TUPLES +] +TRANSLATION_FLAG_IDS = [ + ",".join( + f"{name}={'on' if state else 'off'}" + for name, state in zip(TRANSLATION_SUPPLY_FLAGS, combo) + ) + for combo in _FLAG_STATE_TUPLES +] +del _FLAG_STATE_TUPLES + + +def run_two_second_simulation(flag_overrides): + """Run a 2 s EcoliSim with the provided translation flag overrides.""" + + sim = EcoliSim.from_file() + # Use Parquet emitter for strict type enforcement + sim.config["emitter"] = "parquet" + sim.config["emitter_arg"] = {"out_dir": "out/translation_flag_tests"} + sim.config["max_duration"] = 2 + for flag_key, flag_value in flag_overrides.items(): + sim.config[flag_key] = flag_value + sim.build_ecoli() + sim.run() + @pytest.mark.slow def test_division(agent_id="0", max_duration=4): @@ -315,12 +351,25 @@ def test_emit_unique(): assert isinstance(val["agents"]["0"]["unique"][unique_mol], list) +@pytest.mark.slow +@pytest.mark.parametrize( + "flag_overrides", + TRANSLATION_FLAG_COMBINATIONS, + ids=TRANSLATION_FLAG_IDS, +) +def test_translation_flag_harness(flag_overrides): + """Run the 2 s simulation across every translation flag combination.""" + + run_two_second_simulation(flag_overrides) + + test_library = { "1": test_division, "2": test_division_topology, "3": test_ecoli_generate, "4": test_lattice_lysis, "5": test_emit_unique, + "6": test_translation_flag_harness, } # run experiments in test_library from the command line with: From 09f563e1195d68493204c6c63ebade48126c42fa Mon Sep 17 00:00:00 2001 From: Sean Cheah Date: Sun, 25 Jan 2026 23:03:47 -0800 Subject: [PATCH 2/5] aa_conc already unitless, aa_present is boolean mask --- reconstruction/ecoli/dataclasses/process/metabolism.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reconstruction/ecoli/dataclasses/process/metabolism.py b/reconstruction/ecoli/dataclasses/process/metabolism.py index 47fb6721f..5522c0755 100755 --- a/reconstruction/ecoli/dataclasses/process/metabolism.py +++ b/reconstruction/ecoli/dataclasses/process/metabolism.py @@ -1004,7 +1004,7 @@ def set_phenomological_supply_constants(self, sim_data: "SimulationDataEcoli"): ) def aa_supply_scaling( - self, aa_conc: Unum, aa_present: Unum + self, aa_conc: npt.NDArray[np.float64], aa_present: npt.NDArray[np.bool_] ) -> npt.NDArray[np.float64]: """ Called during polypeptide_elongation process @@ -1021,8 +1021,6 @@ def aa_supply_scaling( higher supply rate if >1, lower supply rate if <1 """ - aa_conc = aa_conc.asNumber(METABOLITE_CONCENTRATION_UNITS) - aa_supply = self.fraction_supply_rate aa_import = aa_present * self.fraction_import_rate aa_synthesis = 1 / (1 + aa_conc / self.KI_aa_synthesis) From 5ae6c22f19b50458488b462363caf0ef1952e7b9 Mon Sep 17 00:00:00 2001 From: Sean Cheah Date: Sun, 25 Jan 2026 22:27:34 -0800 Subject: [PATCH 3/5] Emit aa_exchange_rates without units Raw numbers are easier to work with in DuckDB and elsewhere --- ecoli/library/json_state.py | 17 ----------------- ecoli/processes/metabolism.py | 7 ++----- ecoli/processes/metabolism_redux.py | 7 ++----- ecoli/processes/polypeptide_elongation.py | 13 +++++-------- 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/ecoli/library/json_state.py b/ecoli/library/json_state.py index c9c356f09..6c064601e 100644 --- a/ecoli/library/json_state.py +++ b/ecoli/library/json_state.py @@ -74,23 +74,6 @@ def numpy_molecules(states): ), "constrained": {"GLC[p]": 20.0 * units.mmol / (units.g * units.h)}, } - if "process_state" in states: - if "polypeptide_elongation" in states["process_state"]: - if ( - "aa_exchange_rates" - in states["process_state"]["polypeptide_elongation"] - ): - states["process_state"]["polypeptide_elongation"][ - "aa_exchange_rates" - ] = ( - units.mmol - / units.s - * np.array( - states["process_state"]["polypeptide_elongation"][ - "aa_exchange_rates" - ] - ) - ) return states diff --git a/ecoli/processes/metabolism.py b/ecoli/processes/metabolism.py index b62811681..a118079a3 100644 --- a/ecoli/processes/metabolism.py +++ b/ecoli/processes/metabolism.py @@ -360,13 +360,10 @@ def ports_schema(self): "_divider": "zero", }, "aa_exchange_rates": { - "_default": CONC_UNITS - / TIME_UNITS - * np.zeros(len(self.aa_exchange_names)), + "_default": np.zeros(len(self.aa_exchange_names)), "_emit": True, "_updater": "set", "_divider": "set", - "_serializer": "", }, }, "global_time": {"_default": 0.0}, @@ -495,7 +492,7 @@ def next_update(self, timestep, states): aa_in_media[self.removed_aa_uptake] = False exchange_rates = ( states["polypeptide_elongation"]["aa_exchange_rates"] * timestep - ).asNumber(CONC_UNITS / TIME_UNITS) + ) aa_uptake_package = ( exchange_rates[aa_in_media], self.aa_exchange_names[aa_in_media], diff --git a/ecoli/processes/metabolism_redux.py b/ecoli/processes/metabolism_redux.py index 5991136fb..c6b9f021e 100644 --- a/ecoli/processes/metabolism_redux.py +++ b/ecoli/processes/metabolism_redux.py @@ -356,13 +356,10 @@ def ports_schema(self): }, "gtp_to_hydrolyze": {"_default": 0, "_emit": True, "_divider": "zero"}, "aa_exchange_rates": { - "_default": CONC_UNITS - / TIME_UNITS - * np.zeros(len(self.aa_exchange_names)), + "_default": np.zeros(len(self.aa_exchange_names)), "_emit": True, "_updater": "set", "_divider": "set", - "_serializer": "", }, }, "listeners": { @@ -576,7 +573,7 @@ def next_update(self, timestep, states): aa_in_media[self.removed_aa_uptake] = False exchange_rates = ( states["polypeptide_elongation"]["aa_exchange_rates"] * timestep - ).asNumber(CONC_UNITS / TIME_UNITS) + ) aa_uptake_package = ( exchange_rates[aa_in_media], self.aa_exchange_names[aa_in_media], diff --git a/ecoli/processes/polypeptide_elongation.py b/ecoli/processes/polypeptide_elongation.py index 9f89a9260..f939ed37a 100644 --- a/ecoli/processes/polypeptide_elongation.py +++ b/ecoli/processes/polypeptide_elongation.py @@ -40,6 +40,7 @@ ) from ecoli.processes.registries import topology_registry from ecoli.processes.partition import PartitionedProcess +from ecoli.processes.metabolism import CONC_UNITS, TIME_UNITS MICROMOLAR_UNITS = units.umol / units.L @@ -267,10 +268,6 @@ def __init__(self, parameters=None): self.seed = self.parameters["seed"] self.random_state = np.random.RandomState(seed=self.seed) - self.zero_aa_exchange_rates = ( - MICROMOLAR_UNITS / units.s * np.zeros(len(self.amino_acids)) - ) - def ports_schema(self): return { "environment": { @@ -422,7 +419,7 @@ def ports_schema(self): "_divider": "zero", }, "aa_exchange_rates": { - "_default": self.zero_aa_exchange_rates.copy(), + "_default": np.zeros(len(self.amino_acids)), "_emit": True, "_updater": "set", "_divider": "set", @@ -1159,9 +1156,9 @@ def request( } }, "polypeptide_elongation": { - "aa_exchange_rates": self.counts_to_molar - / units.s - * (import_rates - export_rates) + "aa_exchange_rates": ( + self.counts_to_molar / units.s * (import_rates - export_rates) + ).asNumber(CONC_UNITS / TIME_UNITS) }, }, ) From 49d8490eb656527582e27788c07fae3c7772e24a Mon Sep 17 00:00:00 2001 From: Sean Cheah Date: Mon, 26 Jan 2026 00:48:38 -0800 Subject: [PATCH 4/5] Fix aa_conc units before passing to aa_supply --- ecoli/processes/polypeptide_elongation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ecoli/processes/polypeptide_elongation.py b/ecoli/processes/polypeptide_elongation.py index f939ed37a..31ad2c669 100644 --- a/ecoli/processes/polypeptide_elongation.py +++ b/ecoli/processes/polypeptide_elongation.py @@ -1030,7 +1030,12 @@ def request( # Adjust aa_supply higher if amino acid concentrations are low # Improves stability of charging and mimics amino acid synthesis # inhibition and export - self.process.aa_supply *= self.aa_supply_scaling(aa_conc, aa_in_media) + # Polypeptide elongation operates using concentration units of CONC_UNITS (uM) + # but aa_supply_scaling uses M units, so convert using unit_conversion (1e-6) + self.process.aa_supply *= self.aa_supply_scaling( + self.charging_params["unit_conversion"] * aa_conc.asNumber(CONC_UNITS), + aa_in_media, + ) aa_counts_for_translation = ( v_rib From 48011f091590d6d838fb0c671057953e0b08ec92 Mon Sep 17 00:00:00 2001 From: Sean Cheah Date: Mon, 26 Jan 2026 00:50:30 -0800 Subject: [PATCH 5/5] Bulk requests must be integers TranslationSupplyElongationModel.amino_acid counts returns floats because aa_supply is an array of floats --- ecoli/processes/polypeptide_elongation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ecoli/processes/polypeptide_elongation.py b/ecoli/processes/polypeptide_elongation.py index 31ad2c669..27aefa0d1 100644 --- a/ecoli/processes/polypeptide_elongation.py +++ b/ecoli/processes/polypeptide_elongation.py @@ -752,7 +752,15 @@ def request( ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], dict]: aa_counts_for_translation = self.amino_acid_counts(aasInSequences) - requests = {"bulk": [(self.process.amino_acid_idx, aa_counts_for_translation)]} + # Bulk requests have to be integers (wcEcoli implicitly casts floats to ints) + requests = { + "bulk": [ + ( + self.process.amino_acid_idx, + aa_counts_for_translation.astype(np.int64), + ) + ] + } # Not modeling charging so set fraction charged to 0 for all tRNA fraction_charged = np.zeros(len(self.process.amino_acid_idx))