Skip to content

Commit 103584d

Browse files
BiomodelsIterator has ranges and other fixes.
1 parent d8bfe8a commit 103584d

7 files changed

Lines changed: 148 additions & 26 deletions

scripts/analyze_linear_predictor.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
"BIOMD0000000036", # Errors "too much work"
3030
"BIOMD0000000054", # Long processing time
3131
] """
32-
EXCLUDED_MODELS: list[str] = []
32+
EXCLUDED_MODELS: list[str] = [
33+
"BIOMD0000000035", # Errors "too much work"
34+
"BIOMD0000000036", # Errors "too much work"
35+
"BIOMD0000000079", # Errors "too much work"
36+
"BIOMD0000000088", # Errors "too much work"
37+
]
3338

3439

3540
score = Score(serialization_path=os.path.join(cn.DATA_DIR, "linear_predictor_scores2.csv"))

src/biomodels_cluster.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def clusterAnalysis(
9696
is_report: bool = True,
9797
is_sequential_partition: bool = True,
9898
diameter_metric: str = cn.DIAMETER_IVP,
99-
is_test: bool = False,
99+
first_model_num: int = 0,
100+
last_model_num: int = int(1e9),
100101
) -> pd.DataFrame:
101102
"""
102103
For each model in BioModels, partition its Jacobians into n_cluster clusters and save
@@ -126,8 +127,10 @@ def clusterAnalysis(
126127
Whether to use sequential partitioning instead of k-means clustering.
127128
diameter_metric : str
128129
The metric to use for calculating the diameter of each cluster.
129-
is_test : bool
130-
Whether to run in test mode.
130+
first_model_num : int
131+
The first model number to include (inclusive).
132+
last_model_num : int
133+
The last model number to include (inclusive).
131134
132135
Returns
133136
-------
@@ -142,7 +145,8 @@ def clusterAnalysis(
142145
excluded_models=excluded_models,
143146
existing_csv_path=output_data_file,
144147
is_report=is_report,
145-
is_test=is_test,
148+
first_model_num=first_model_num,
149+
last_model_num=last_model_num,
146150
)
147151
existing_df = iterator._existing_df
148152
##

src/biomodels_iterator.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
import pandas as pd # type: ignore
88
from typing import Iterator, List, Optional, Tuple
99

10-
NUM_TEST_MODELS = 5
11-
12-
1310
############################################
1411
class BiomodelsItem:
1512
"""Represents a single BioModel with its associated file paths."""
1613

17-
def __init__(self, model_name: str, sbml_paths: List[str], sedml_paths: List[str],
18-
existing_df: pd.DataFrame = pd.DataFrame()) -> None:
14+
def __init__(self,
15+
model_name: str,
16+
sbml_paths: List[str],
17+
sedml_paths: List[str],
18+
existing_df: pd.DataFrame = pd.DataFrame(),
19+
last_model_num: int = int(1e9)) -> None:
1920
"""
2021
Initialize a BiomodelsItem.
2122
@@ -34,9 +35,8 @@ def __init__(self, model_name: str, sbml_paths: List[str], sedml_paths: List[str
3435
self.model_name = model_name
3536
self.sbml_paths = sbml_paths
3637
self.sedml_paths = sedml_paths
37-
if existing_df is None:
38-
self.existing_df = pd.DataFrame()
39-
else:
38+
self.existing_df = pd.DataFrame()
39+
if existing_df is not None:
4040
self.existing_df = existing_df
4141

4242
def __repr__(self) -> str:
@@ -57,7 +57,8 @@ def __init__(self,
5757
excluded_models: List[str] = [],
5858
existing_csv_path: Optional[str] = None,
5959
is_report: bool = True,
60-
is_test: bool = False,
60+
first_model_num: int = 0,
61+
last_model_num: int = int(1e9)
6162
) -> None:
6263
"""
6364
Initialize a BiomodelsIterator.
@@ -75,15 +76,18 @@ def __init__(self,
7576
Path to an existing CSV file containing processed models. If provided,
7677
models listed in this file will be added to the excluded_models list.
7778
The column cn.COL_MODEL_NAME will be used to identify processed models.
78-
is_test : bool
79-
Whether to run in test mode, which may limit the number of models processed or alter behavior
79+
first_model_num : int
80+
The first model number to include (inclusive).
81+
last_model_num : int
82+
The last model number to include (inclusive).
8083
"""
8184
self.biomodels_dir = biomodels_dir
8285
self.excluded_models = excluded_models
8386
self._is_report = is_report
8487
self._existing_csv_path = existing_csv_path
8588
self._existing_df, self._processed_models = self._getProcessedModelsFromCSV()
86-
self._is_test = is_test
89+
self.first_model_num = first_model_num
90+
self.last_model_num = last_model_num
8791

8892
def _getProcessedModelsFromCSV(self) -> Tuple[pd.DataFrame, List[str]]:
8993
"""
@@ -142,6 +146,14 @@ def getBiomodelInfo(cls, model_dir: str) -> BiomodelsItem:
142146
sedml_paths=sedml_paths,
143147
existing_df=pd.DataFrame()
144148
)
149+
150+
@staticmethod
151+
def extractModelNum(model_name: str) -> int:
152+
"""Extracts the numeric part of a model name like 'BIOMD0000000001'."""
153+
try:
154+
return int(model_name.replace("BIOMD", ""))
155+
except ValueError:
156+
return -1 # Return -1 for unexpected model name formats
145157

146158
def __iter__(self) -> Iterator[BiomodelsItem]:
147159
"""
@@ -157,10 +169,11 @@ def __iter__(self) -> Iterator[BiomodelsItem]:
157169
if os.path.isdir(os.path.join(self.biomodels_dir, d))
158170
and "BIOMD" in d
159171
)
160-
for idx, model_name in enumerate(model_names):
161-
if idx > NUM_TEST_MODELS and self._is_test:
162-
self._msg(f"Test mode enabled, stopping after {NUM_TEST_MODELS} models.")
163-
break
172+
for model_name in model_names:
173+
model_num = self.extractModelNum(model_name)
174+
if model_num < self.first_model_num or model_num > self.last_model_num:
175+
self._msg(f"Skipping model {model_name} with number {model_num}")
176+
continue
164177
model_dir = os.path.join(self.biomodels_dir, model_name)
165178
if model_name in self._processed_models:
166179
self._msg(f"Skipping processed model: {model_name}")

src/trajectory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ def _residuals(params: Parameters) -> np.ndarray:
473473
fitted_jacobian_arr[i, i] = result.params[f'd{i}'].value # type: ignore
474474
if is_adjusted_result:
475475
self._fitted_jacobian_arr = utils.adjustJacobian(fitted_jacobian_arr, duration)
476+
sel = np.isnan(self._fitted_jacobian_arr) | np.isinf(self._fitted_jacobian_arr)
477+
self._fitted_jacobian_arr[sel] = 0.0
476478
else:
477479
self._fitted_jacobian_arr = fitted_jacobian_arr
478480
return self._fitted_jacobian_arr

tests/test_biomodels_cluster.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
IGNORE_TESTS = False
1818
HAS_BIOMODELS = os.path.isdir(cn.BIOMODELS_DIR)
1919
TEST_MODEL = "BIOMD0000000001"
20+
FIRST_MODEL_NUM = 1
21+
LAST_MODEL_NUM = 10
2022

2123
# Minimal SBML for a two-species decay used in unit tests that don't need
2224
# real BioModels data.
@@ -256,7 +258,7 @@ def test_returns_dataframe(self) -> None:
256258
is_report=False,
257259
is_sequential_partition=True,
258260
diameter_metric=cn.DIAMETER_MAX_CV,
259-
is_test=True, # type: ignore
261+
first_model_num=FIRST_MODEL_NUM, last_model_num=LAST_MODEL_NUM,
260262
)
261263
self.assertIsInstance(df, pd.DataFrame)
262264

@@ -271,7 +273,7 @@ def test_output_csv_created(self) -> None:
271273
is_report=False,
272274
is_sequential_partition=True,
273275
diameter_metric=cn.DIAMETER_MAX_CV,
274-
is_test=True, # type: ignore
276+
first_model_num=FIRST_MODEL_NUM, last_model_num=LAST_MODEL_NUM,
275277
)
276278
self.assertTrue(os.path.isfile(self._output_csv))
277279

@@ -285,7 +287,7 @@ def test_dataframe_has_expected_columns(self) -> None:
285287
is_report=IGNORE_TESTS,
286288
is_sequential_partition=True,
287289
diameter_metric=cn.DIAMETER_MAX_CV,
288-
is_test=True,
290+
first_model_num=FIRST_MODEL_NUM, last_model_num=LAST_MODEL_NUM,
289291
)
290292
for col in [cn.COL_MAXCV, cn.COL_ENDTIME]:
291293
self.assertIn(col, df.columns)
@@ -301,7 +303,7 @@ def test_excluded_model_not_in_result(self) -> None:
301303
is_report=IGNORE_TESTS,
302304
is_sequential_partition=True,
303305
diameter_metric=cn.DIAMETER_MAX_CV,
304-
is_test=True,
306+
first_model_num=FIRST_MODEL_NUM, last_model_num=LAST_MODEL_NUM,
305307
)
306308
if TEST_MODEL in df.index:
307309
self.fail(f"{TEST_MODEL} should have been excluded")

tests/test_biomodels_iterator.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,102 @@ def test_item_existing_df_none_without_csv(self) -> None:
426426
self.assertTrue(items[0].existing_df.empty)
427427

428428

429+
class TestExtractModelNum(unittest.TestCase):
430+
"""Tests for BiomodelsIterator.extractModelNum."""
431+
432+
def test_standard_model_name(self) -> None:
433+
"""Extracts the integer from a standard BIOMD name."""
434+
if IGNORE_TESTS:
435+
return
436+
self.assertEqual(BiomodelsIterator.extractModelNum("BIOMD0000000001"), 1)
437+
438+
def test_large_model_number(self) -> None:
439+
"""Extracts a larger model number correctly."""
440+
if IGNORE_TESTS:
441+
return
442+
self.assertEqual(BiomodelsIterator.extractModelNum("BIOMD0000001234"), 1234)
443+
444+
def test_invalid_name_returns_minus_one(self) -> None:
445+
"""Non-numeric suffix returns -1."""
446+
if IGNORE_TESTS:
447+
return
448+
self.assertEqual(BiomodelsIterator.extractModelNum("NOT_A_MODEL"), -1)
449+
450+
def test_empty_string_returns_minus_one(self) -> None:
451+
"""Empty string returns -1."""
452+
if IGNORE_TESTS:
453+
return
454+
self.assertEqual(BiomodelsIterator.extractModelNum(""), -1)
455+
456+
def test_biomd_with_no_digits_returns_minus_one(self) -> None:
457+
"""'BIOMD' with no trailing digits returns -1."""
458+
if IGNORE_TESTS:
459+
return
460+
self.assertEqual(BiomodelsIterator.extractModelNum("BIOMD"), -1)
461+
462+
463+
class TestBiomodelsIteratorRange(unittest.TestCase):
464+
"""Tests for first_model_num / last_model_num range filtering in __iter__."""
465+
466+
def setUp(self) -> None:
467+
self._tmpdir = tempfile.mkdtemp()
468+
for name in ["BIOMD0000000001", "BIOMD0000000002", "BIOMD0000000003"]:
469+
_make_model_dir(self._tmpdir, name, xml_files=["model.xml"], sedml_files=[])
470+
471+
def tearDown(self) -> None:
472+
import shutil
473+
shutil.rmtree(self._tmpdir, ignore_errors=True)
474+
475+
def _names(self, **kwargs) -> List[str]:
476+
it = BiomodelsIterator(biomodels_dir=self._tmpdir, is_report=False, **kwargs)
477+
return [item.model_name for item in it]
478+
479+
def test_default_range_yields_all_models(self) -> None:
480+
"""Default range (0 to 1e9) yields all models."""
481+
if IGNORE_TESTS:
482+
return
483+
self.assertEqual(self._names(), ["BIOMD0000000001", "BIOMD0000000002", "BIOMD0000000003"])
484+
485+
def test_first_model_num_excludes_lower_models(self) -> None:
486+
"""Models with number below first_model_num are not yielded."""
487+
if IGNORE_TESTS:
488+
return
489+
names = self._names(first_model_num=2)
490+
self.assertNotIn("BIOMD0000000001", names)
491+
self.assertIn("BIOMD0000000002", names)
492+
self.assertIn("BIOMD0000000003", names)
493+
494+
def test_last_model_num_excludes_higher_models(self) -> None:
495+
"""Models with number above last_model_num are not yielded."""
496+
if IGNORE_TESTS:
497+
return
498+
names = self._names(last_model_num=2)
499+
self.assertIn("BIOMD0000000001", names)
500+
self.assertIn("BIOMD0000000002", names)
501+
self.assertNotIn("BIOMD0000000003", names)
502+
503+
def test_range_is_inclusive_on_both_ends(self) -> None:
504+
"""first_model_num and last_model_num are both inclusive."""
505+
if IGNORE_TESTS:
506+
return
507+
names = self._names(first_model_num=2, last_model_num=2)
508+
self.assertEqual(names, ["BIOMD0000000002"])
509+
510+
def test_narrow_range_yields_subset(self) -> None:
511+
"""A range narrower than the full set yields only the matching models."""
512+
if IGNORE_TESTS:
513+
return
514+
names = self._names(first_model_num=1, last_model_num=2)
515+
self.assertEqual(names, ["BIOMD0000000001", "BIOMD0000000002"])
516+
517+
def test_empty_range_yields_nothing(self) -> None:
518+
"""A range that matches no models yields an empty list."""
519+
if IGNORE_TESTS:
520+
return
521+
names = self._names(first_model_num=10, last_model_num=20)
522+
self.assertEqual(names, [])
523+
524+
429525
@unittest.skipUnless(HAS_BIOMODELS, "BioModels data directory not found")
430526
class TestBiomodelsIteratorReal(unittest.TestCase):
431527
"""Integration tests for BiomodelsIterator using the real BioModels directory."""

tests/test_score.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def _checkBiomodel(self, model_num: int) -> None:
423423
true_df = l_roadrunner.timecourse
424424
trajectory = Trajectory(l_roadrunner)
425425
try:
426-
pred_df = trajectory.predictLinear()
426+
pred_df = trajectory.predictLinear(is_adjust_fitted_jacobian=True)
427427
except ValueError as e:
428428
self.skipTest(
429429
f"predictLinear raised ValueError for model {model_num}: {e}")

0 commit comments

Comments
 (0)