diff --git a/src/job_scripts/reconstruct.py b/src/job_scripts/reconstruct.py index 83a2a8e..2667041 100644 --- a/src/job_scripts/reconstruct.py +++ b/src/job_scripts/reconstruct.py @@ -55,6 +55,16 @@ def merge_ws_metadata(ws, obj_path, new_meta): ws.update_metadata({"objects": [[obj_path, merged]]}) +def _set_biomass_objective(model) -> None: + """Set the model objective to bio1 if present, otherwise fall back to the first + biomass-like reaction. Silently skips if no biomass reaction is found.""" + if "bio1" in {r.id for r in model.reactions}: + model.objective = "bio1" + return + biomass_rxns = [r for r in model.reactions if "bio" in r.id.lower()] + if biomass_rxns: + model.objective = biomass_rxns[0].id + def main(): parser = argparse.ArgumentParser(description="Model reconstruction job") parser.add_argument("--job-id", required=True) @@ -346,7 +356,7 @@ def main(): ws = get_storage_service(args.token) import cobra.io - mdlutl.model.objective = "bio1" + _set_biomass_objective(mdlutl.model) cobra_json = json.dumps(cobra.io.model_to_dict(mdlutl.model)) if not hasattr(mdlutl.model, 'get_data'): diff --git a/src/modelseed_api/jobs/tasks.py b/src/modelseed_api/jobs/tasks.py index 9a517b8..c5c70f8 100644 --- a/src/modelseed_api/jobs/tasks.py +++ b/src/modelseed_api/jobs/tasks.py @@ -450,6 +450,16 @@ def _fix_gapfilling_metadata(ws_data: dict, media_workspace_ref: str | None) -> gd[new_id] = gd.pop(old_id) +def _set_biomass_objective(model) -> None: + """Set the model objective to bio1 if present, otherwise fall back to the first + biomass-like reaction. Silently skips if no biomass reaction is found.""" + if "bio1" in {r.id for r in model.reactions}: + model.objective = "bio1" + return + biomass_rxns = [r for r in model.reactions if "bio" in r.id.lower()] + if biomass_rxns: + model.objective = biomass_rxns[0].id + @app.task(bind=True, name="modelseed.reconstruct") def reconstruct( self, @@ -731,7 +741,7 @@ def reconstruct( # Save cobra JSON BEFORE CobraModelConverter (which loses exchange # bounds and breaks FBA). cobra.io roundtrip is lossless. import cobra.io - mdlutl.model.objective = "bio1" + _set_biomass_objective(mdlutl.model) cobra_json = json.dumps(cobra.io.model_to_dict(mdlutl.model)) if not hasattr(mdlutl.model, 'get_data'): @@ -1262,10 +1272,7 @@ def _reconstruct_one_genome( if do_fva: # Configure model objective for FVA; both runs use the same model # but with different media bounds applied (handled by media-pkg). - try: - mdlutl.model.objective = "bio1" - except Exception: - pass + _set_biomass_objective(mdlutl.model) try: fva_rich = compute_fva_classes(mdlutl.model, media_or_setup="rich") fva_min = compute_fva_classes(mdlutl.model, media_or_setup="minimal") diff --git a/tests/unit/test_set_biomass_objective.py b/tests/unit/test_set_biomass_objective.py new file mode 100644 index 0000000..8297980 --- /dev/null +++ b/tests/unit/test_set_biomass_objective.py @@ -0,0 +1,49 @@ +"""Unit tests for _set_biomass_objective helper.""" +import pytest + +pytestmark = pytest.mark.unit + + +def _make_mock_model(reaction_ids): + """Build a minimal mock cobra Model with the given reaction IDs.""" + import unittest.mock as mock + + reactions = [] + for rid in reaction_ids: + r = mock.MagicMock() + r.id = rid + r.__str__ = lambda self: self.id + reactions.append(r) + + model = mock.MagicMock() + model.reactions = reactions + model.objective = None + return model + + +class TestSetBiomassObjective: + def test_bio1_present_sets_bio1(self): + from modelseed_api.jobs.tasks import _set_biomass_objective + model = _make_mock_model(["rxn00001", "bio1", "rxn00002"]) + _set_biomass_objective(model) + assert model.objective == "bio1" + + def test_no_bio1_falls_back_to_biomass_reaction(self): + from modelseed_api.jobs.tasks import _set_biomass_objective + model = _make_mock_model(["rxn00001", "bio2", "rxn00002"]) + _set_biomass_objective(model) + assert model.objective == "bio2" + + def test_no_biomass_reaction_skips_silently(self): + from modelseed_api.jobs.tasks import _set_biomass_objective + model = _make_mock_model(["rxn00001", "rxn00002"]) + original = model.objective + _set_biomass_objective(model) + assert model.objective == original + + def test_empty_model_skips_silently(self): + from modelseed_api.jobs.tasks import _set_biomass_objective + model = _make_mock_model([]) + original = model.objective + _set_biomass_objective(model) + assert model.objective == original \ No newline at end of file