Skip to content

Commit f561f2d

Browse files
authored
Merge pull request #20 from heberlr/development
Version 1.2.3
2 parents e893b5d + d1f9ad5 commit f561f2d

File tree

17 files changed

+850
-131
lines changed

17 files changed

+850
-131
lines changed

.github/workflows/test-examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Run Unit Tests
22

3-
on: [push, pull_request]
3+
on: [pull_request]
44

55
jobs:
66
test:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
[![Documentation Status](https://readthedocs.org/projects/uq-physicell/badge/?version=latest)](https://uq-physicell.readthedocs.io/en/latest/?badge=latest)
99
[![PyPI](https://badge.fury.io/py/uq-physicell.svg?updated=20251110)](https://badge.fury.io/py/uq-physicell)
1010
[![Python](https://img.shields.io/badge/Python-%3E%3D3.10-blue?logo=python&logoColor=green)](https://python.org)
11+
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://github.com/heberlr/UQ_PhysiCell/tree/development/uq_physicell/LICENSE.md)
1112

1213
UQ-PhysiCell is a comprehensive framework for performing uncertainty quantification and parameter calibration of PhysiCell models. It provides sophisticated tools for model analysis, calibration, and model selection.
1314

doc/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Welcome to the UQ-PhysiCell documentation! This project provides uncertainty quantification tools for PhysiCell models.
44

5-
**Release:** [v1.2.2 (GitHub link)](https://github.com/heberlr/UQ_PhysiCell/releases/tag/v1.2.2 (GitHub link))
5+
**Release:** [v1.2.3 (GitHub link)](https://github.com/heberlr/UQ_PhysiCell/releases/tag/v1.2.3 (GitHub link))
66

77
```{note}
88
This readthedocs documentation is under active development.

pyproject.toml

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,27 @@
66
# https://hatch.pypa.io/latest/
77
#
88
# releasing a next version on pypi:
9-
# 0. change uq_physicell/VERSION.py # increase version number in file
9+
# 0. change uq_physicell/VERSION.py # increase version number in file - # If there is no changes in the core module DO NOT CREATE A NEW VERSION
10+
# 1. Run bash doc/rebuild_docs.sh --clean # Update version on documentation
11+
# 2. Commit and push the changes
12+
# 3. Create a PR to development branch from your branch
13+
# Development branch
14+
# 4. Check if rebuild the documentation in : https://app.readthedocs.org/projects/uq-physicell/
1015
# Prepate to update pypi version
11-
# 1. rm -r dist # clean previous distribution
12-
# 2. python -m build --sdist # make source distribution
13-
# 3. python -m build --wheel # make binary distribution python wheel
14-
# 4. python -m twine upload dist/* --verbose # publising python package
15-
# 5. git push origin
16-
# 6. Pull request from dev to main
17-
# 7. Create the release tag (make sure the release tag is vx.x.x to match with GitHub link from documentation)
18-
# Prepare to reload the documentation - (Execute from here just for documentation update).
19-
# 8. Run bash doc/rebuild_docs.sh --clean
20-
# 9. Rebuild the documentation in : https://app.readthedocs.org/projects/uq-physicell/
16+
# 5. rm -r dist # clean previous distribution
17+
# 6. python -m build --sdist # make source distribution
18+
# 7. python -m build --wheel # make binary distribution python wheel
19+
# 8. python -m twine upload dist/* --verbose # publising python package
20+
# 9. Change the badge of pypi on README.md https://badge.fury.io/py/uq-physicell.svg?updated=xxxxxxxx
21+
# 10. Pull request from dev to main
22+
# Main branch
23+
# 11. Create the release tag (make sure the release tag is vx.x.x to match with GitHub link from documentation)
24+
25+
26+
27+
28+
29+
2130
##### do not use conda env #######
2231

2332
[build-system]
@@ -33,7 +42,7 @@ readme = "README.md"
3342

3443
requires-python = ">=3.10"
3544

36-
license = {text = "MIT"}
45+
license = {file = "uq_physicell/LICENSE.md"}
3746

3847
authors = [
3948
{name = "Heber L. Rocha", email = "heberonly@gmail.com"}

tests/test_bo_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def setUp(self):
3232

3333
self.df_search_space = pd.DataFrame({
3434
'ParamName': ['param1', 'param2'],
35-
'Lower_Bound': [0.0, 1.0],
36-
'Upper_Bound': [1.0, 2.0]
35+
'lower_bound': [0.0, 1.0],
36+
'upper_bound': [1.0, 2.0]
3737
})
3838

3939
self.search_space = {

tests/test_ma_database.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#!/usr/bin/env python
2+
"""Test suite for Model Analysis database modular load functions."""
3+
4+
import os
5+
import tempfile
6+
import pickle
7+
import pytest
8+
import pandas as pd
9+
import numpy as np
10+
11+
from uq_physicell.database.ma_db import (
12+
create_structure,
13+
insert_metadata,
14+
insert_param_space,
15+
insert_qois,
16+
insert_samples,
17+
insert_output,
18+
load_metadata,
19+
load_parameter_space,
20+
load_qois,
21+
load_samples,
22+
load_output,
23+
load_data_unserialized,
24+
load_structure
25+
)
26+
27+
28+
@pytest.fixture
29+
def sample_database():
30+
"""Create a temporary database with sample data for testing."""
31+
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp:
32+
db_file = tmp.name
33+
34+
# Create structure
35+
create_structure(db_file)
36+
37+
# Insert test data
38+
insert_metadata(db_file, "Sobol", "/path/to/config.ini", "test_model")
39+
40+
params = {
41+
'param1': {'lower_bound': 0.0, 'upper_bound': 1.0, 'ref_value': 0.5, 'perturbation': [0.1, 0.2]},
42+
'param2': {'lower_bound': 0.0, 'upper_bound': 10.0, 'ref_value': 5.0, 'perturbation': [1.0, 2.0]}
43+
}
44+
insert_param_space(db_file, params)
45+
46+
qois = {
47+
'total_cells': 'lambda data: data["cells"].sum()',
48+
'max_radius': 'lambda data: data["radius"].max()'
49+
}
50+
insert_qois(db_file, qois)
51+
52+
samples = {
53+
0: {'param1': 0.3, 'param2': 3.0},
54+
1: {'param1': 0.7, 'param2': 7.0}
55+
}
56+
insert_samples(db_file, samples)
57+
58+
# Create mock output data
59+
for sample_id in [0, 1]:
60+
for replicate_id in [0, 1]:
61+
data = pd.DataFrame({
62+
'time': [0, 1, 2],
63+
'total_cells': [10, 20, 30],
64+
'max_radius': [5.0, 10.0, 15.0]
65+
})
66+
serialized = pickle.dumps(data)
67+
insert_output(db_file, sample_id, replicate_id, serialized)
68+
69+
yield db_file
70+
71+
# Cleanup
72+
if os.path.exists(db_file):
73+
os.remove(db_file)
74+
75+
76+
class TestModularLoadFunctions:
77+
"""Test suite for modular database load functions."""
78+
79+
def test_load_metadata(self, sample_database):
80+
"""Test load_metadata function."""
81+
df_metadata = load_metadata(sample_database)
82+
assert df_metadata.shape[0] == 1
83+
assert df_metadata['Sampler'].values[0] == 'Sobol'
84+
assert df_metadata['Ini_File_Path'].values[0] == '/path/to/config.ini'
85+
assert df_metadata['StructureName'].values[0] == 'test_model'
86+
87+
def test_load_parameter_space(self, sample_database):
88+
"""Test load_parameter_space function."""
89+
df_params = load_parameter_space(sample_database)
90+
assert df_params.shape[0] == 2
91+
assert 'param1' in df_params['ParamName'].values
92+
assert 'param2' in df_params['ParamName'].values
93+
94+
# Check that perturbation is converted to numpy array
95+
assert isinstance(df_params['perturbation'].iloc[0], np.ndarray)
96+
assert len(df_params['perturbation'].iloc[0]) == 2
97+
98+
def test_load_qois(self, sample_database):
99+
"""Test load_qois function."""
100+
df_qois = load_qois(sample_database)
101+
assert df_qois.shape[0] == 2
102+
assert 'total_cells' in df_qois['QOI_Name'].values
103+
assert 'max_radius' in df_qois['QOI_Name'].values
104+
105+
def test_load_samples(self, sample_database):
106+
"""Test load_samples function."""
107+
dic_samples = load_samples(sample_database)
108+
assert len(dic_samples) == 2
109+
assert dic_samples[0]['param1'] == 0.3
110+
assert dic_samples[0]['param2'] == 3.0
111+
assert dic_samples[1]['param1'] == 0.7
112+
assert dic_samples[1]['param2'] == 7.0
113+
114+
def test_load_output_full(self, sample_database):
115+
"""Test load_output with full data loading."""
116+
df_output = load_output(sample_database, load_data=True)
117+
assert df_output.shape[0] == 4 # 2 samples * 2 replicates
118+
assert 'SampleID' in df_output.columns
119+
assert 'ReplicateID' in df_output.columns
120+
assert 'Data' in df_output.columns
121+
122+
# Check that data is deserialized
123+
assert isinstance(df_output['Data'].iloc[0], pd.DataFrame)
124+
125+
def test_load_output_metadata_only(self, sample_database):
126+
"""Test load_output with metadata only (no data loading)."""
127+
df_output = load_output(sample_database, load_data=False)
128+
assert df_output.shape[0] == 4
129+
assert 'SampleID' in df_output.columns
130+
assert 'ReplicateID' in df_output.columns
131+
assert 'Data' not in df_output.columns
132+
133+
def test_load_output_filter_by_sample(self, sample_database):
134+
"""Test load_output with sample_ids filter."""
135+
df_output = load_output(sample_database, sample_ids=[0], load_data=True)
136+
assert df_output.shape[0] == 2 # Only sample 0, both replicates
137+
assert all(df_output['SampleID'] == 0)
138+
139+
def test_load_output_filter_by_replicate(self, sample_database):
140+
"""Test load_output with replicate_ids filter."""
141+
df_output = load_output(sample_database, replicate_ids=[0], load_data=True)
142+
assert df_output.shape[0] == 2 # Only replicate 0, both samples
143+
assert all(df_output['ReplicateID'] == 0)
144+
145+
def test_load_output_filter_combined(self, sample_database):
146+
"""Test load_output with both sample_ids and replicate_ids filters."""
147+
df_output = load_output(sample_database, sample_ids=[1], replicate_ids=[1], load_data=True)
148+
assert df_output.shape[0] == 1
149+
assert df_output['SampleID'].values[0] == 1
150+
assert df_output['ReplicateID'].values[0] == 1
151+
152+
def test_load_data_unserialized_full(self, sample_database):
153+
"""Test load_data_unserialized with full data."""
154+
df_unserialized = load_data_unserialized(sample_database)
155+
assert df_unserialized.shape[0] == 4
156+
157+
# Check that QoI columns are expanded
158+
assert 'total_cells_0' in df_unserialized.columns
159+
assert 'total_cells_1' in df_unserialized.columns
160+
assert 'total_cells_2' in df_unserialized.columns
161+
assert 'time_0' in df_unserialized.columns
162+
assert 'max_radius_0' in df_unserialized.columns
163+
164+
def test_load_data_unserialized_filtered(self, sample_database):
165+
"""Test load_data_unserialized with sample filter."""
166+
df_unserialized = load_data_unserialized(sample_database, sample_ids=[0])
167+
assert df_unserialized.shape[0] == 2
168+
assert all(df_unserialized['SampleID'] == 0)
169+
170+
def test_load_structure_backward_compatibility_full(self, sample_database):
171+
"""Test load_structure with load_result=True (backward compatibility)."""
172+
metadata, params, qois, samples, results = load_structure(sample_database, load_result=True)
173+
174+
assert isinstance(metadata, pd.DataFrame)
175+
assert metadata.shape[0] == 1
176+
177+
assert isinstance(params, pd.DataFrame)
178+
assert params.shape[0] == 2
179+
180+
assert isinstance(qois, pd.DataFrame)
181+
assert qois.shape[0] == 2
182+
183+
assert isinstance(samples, dict)
184+
assert len(samples) == 2
185+
186+
assert isinstance(results, pd.DataFrame)
187+
assert results.shape[0] == 4
188+
assert 'total_cells_0' in results.columns # QoI expansion
189+
190+
def test_load_structure_backward_compatibility_metadata(self, sample_database):
191+
"""Test load_structure with load_result=False (backward compatibility)."""
192+
metadata, params, qois, samples, ids = load_structure(sample_database, load_result=False)
193+
194+
assert isinstance(metadata, pd.DataFrame)
195+
assert isinstance(params, pd.DataFrame)
196+
assert isinstance(qois, pd.DataFrame)
197+
assert isinstance(samples, dict)
198+
assert isinstance(ids, pd.DataFrame)
199+
200+
assert ids.shape[0] == 4
201+
assert 'SampleID' in ids.columns
202+
assert 'ReplicateID' in ids.columns
203+
assert 'Data' not in ids.columns
204+
205+
206+
class TestDatabaseCreation:
207+
"""Test suite for database creation functions."""
208+
209+
def test_create_structure(self):
210+
"""Test create_structure function."""
211+
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp:
212+
db_file = tmp.name
213+
214+
try:
215+
create_structure(db_file)
216+
assert os.path.exists(db_file)
217+
218+
# Verify tables exist
219+
import sqlite3
220+
conn = sqlite3.connect(db_file)
221+
cursor = conn.cursor()
222+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
223+
tables = [row[0] for row in cursor.fetchall()]
224+
conn.close()
225+
226+
assert 'Metadata' in tables
227+
assert 'ParameterSpace' in tables
228+
assert 'QoIs' in tables
229+
assert 'Samples' in tables
230+
assert 'Output' in tables
231+
finally:
232+
if os.path.exists(db_file):
233+
os.remove(db_file)
234+
235+
def test_insert_functions(self):
236+
"""Test all insert functions."""
237+
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp:
238+
db_file = tmp.name
239+
240+
try:
241+
create_structure(db_file)
242+
243+
# Test insert_metadata
244+
insert_metadata(db_file, "TestSampler", "/test/path", "test_struct")
245+
df = load_metadata(db_file)
246+
assert df['Sampler'].values[0] == 'TestSampler'
247+
248+
# Test insert_param_space
249+
params = {'p1': {'lower_bound': 0, 'upper_bound': 1, 'ref_value': 0.5, 'perturbation': [0.1]}}
250+
insert_param_space(db_file, params)
251+
df = load_parameter_space(db_file)
252+
assert df.shape[0] == 1
253+
254+
# Test insert_qois
255+
qois = {'qoi1': 'lambda x: x'}
256+
insert_qois(db_file, qois)
257+
df = load_qois(db_file)
258+
assert df.shape[0] == 1
259+
260+
# Test insert_samples
261+
samples = {0: {'p1': 0.5}}
262+
insert_samples(db_file, samples)
263+
dic = load_samples(db_file)
264+
assert len(dic) == 1
265+
266+
# Test insert_output
267+
data = pd.DataFrame({'col': [1, 2, 3]})
268+
serialized = pickle.dumps(data)
269+
insert_output(db_file, 0, 0, serialized)
270+
df = load_output(db_file, load_data=False)
271+
assert df.shape[0] == 1
272+
273+
finally:
274+
if os.path.exists(db_file):
275+
os.remove(db_file)
276+
277+
278+
if __name__ == '__main__':
279+
pytest.main([__file__, '-v'])

uq_physicell/LICENSE.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2025, Heber Rocha and the UQ_PhysiCell Project
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its contributors
17+
may be used to endorse or promote products derived from this software
18+
without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

uq_physicell/VERSION.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.2.2"
1+
__version__ = "1.2.3"

0 commit comments

Comments
 (0)