diff --git a/README.md b/README.md index da66993c..e8c1201f 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,273 @@ Please follow the instructions in [python_testing_exercise.md](https://github.co ### pytest log +```text +(venv) yonatan@Yonatans-MacBook-Pro testing-python-exercise-wt2425 % python -m pytest tests/unit/test_diffusion2d_functions.py +============================================================================================================= test session starts ============================================================================================================== +platform darwin -- Python 3.11.5, pytest-8.3.4, pluggy-1.5.0 +rootdir: /Users/yonatan/Documents/SSE/testing-python-exercise-wt2425 +collected 3 items + +tests/unit/test_diffusion2d_functions.py FFF [100%] + +=================================================================================================================== FAILURES =================================================================================================================== +____________________________________________________________________________________________________________ test_initialize_domain ____________________________________________________________________________________________________________ + + def test_initialize_domain(): + """ + Check function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # domain parameters + w = 12.0 + h = 10.0 + dx = 0.2 + dy = 0.1 + + # Expected nx, ny + nx = int(w / dx) # 12 / 0.2 = 60 + ny = int(h / dy) # 10 / 0.1 = 100 + + solver.initialize_domain(w=w, h=h, dx=dx, dy=dy) + +> assert solver.nx == nx, f"nx should be {nx} but got {solver.nx}" +E AssertionError: nx should be 60 but got 50 +E assert 50 == 60 +E + where 50 = .nx + +tests/unit/test_diffusion2d_functions.py:25: AssertionError +_____________________________________________________________________________________________________ test_initialize_physical_parameters ______________________________________________________________________________________________________ + + def test_initialize_physical_parameters(): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + + solver.dx = 0.1 + solver.dy = 0.1 + + # Set parameters + d = 5.0 + T_cold = 300.0 + T_hot = 600.0 + + # Expected dt = dx^2 * dy^2 / (2 * d * (dx^2 + dy^2)) + # dx^2 = 0.01, dy^2 = 0.01 => dx^2 + dy^2 = 0.02 + # => numerator= 0.01 * 0.01= 1.e-4 + # => denominator= 2 * 5.0 * 0.02= 0.2 + # => dt_expected= 1.e-4 / 0.2= 5.e-4=0.0005 + dt_expected = 0.0005 + + # Call the function + solver.initialize_physical_parameters(d=d, T_cold=T_cold, T_hot=T_hot) + + # Check +> assert solver.D == d, f"D should be {d} but got {solver.D}" +E AssertionError: D should be 5.0 but got 15.0 +E assert 15.0 == 5.0 +E + where 15.0 = .D + +tests/unit/test_diffusion2d_functions.py:55: AssertionError +------------------------------------------------------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------------------------------------------------- +dt = 0.00016666666666666672 +__________________________________________________________________________________________________________ test_set_initial_condition __________________________________________________________________________________________________________ + + def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + + # Define everything needed by set_initial_condition + solver.nx = 100 + solver.ny = 200 + solver.dx = 0.1 + solver.dy = 0.1 + solver.T_cold = 300.0 + solver.T_hot = 700.0 + + u = solver.set_initial_condition() + + # Check shape + assert u.shape == (100, 200), \ + f"Expected array shape (100, 200), got {u.shape}" + + # The center is (cx, cy) = (5, 5), radius=2 => i_center= 5/0.1=50, j_center= 5/0.1=50 + # Check the center is T_hot + i_center = 50 + j_center = 50 +> assert np.isclose(u[i_center, j_center], solver.T_hot), \ + "Center of hot disc should have temperature T_hot." +E AssertionError: Center of hot disc should have temperature T_hot. +E assert np.False_ +E + where np.False_ = (np.float64(300.0), 700.0) +E + where = np.isclose +E + and 700.0 = .T_hot + +tests/unit/test_diffusion2d_functions.py:87: AssertionError +=========================================================================================================== short test summary info ============================================================================================================ +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_domain - AssertionError: nx should be 60 but got 50 +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_physical_parameters - AssertionError: D should be 5.0 but got 15.0 +FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition - AssertionError: Center of hot disc should have temperature T_hot. +============================================================================================================== 3 failed in 0.46s =============================================================================================================== +``` + +```text +(venv) yonatan@Yonatans-MacBook-Pro testing-python-exercise-wt2425 % python -m pytest tests/integration/test_diffusion2d.py + +============================================================================================================= test session starts ============================================================================================================== +platform darwin -- Python 3.11.5, pytest-8.3.4, pluggy-1.5.0 +rootdir: /Users/yonatan/Documents/SSE/testing-python-exercise-wt2425 +collected 2 items + +tests/integration/test_diffusion2d.py FF [100%] + +=================================================================================================================== FAILURES =================================================================================================================== +_____________________________________________________________________________________________________ test_initialize_physical_parameters ______________________________________________________________________________________________________ + + def test_initialize_physical_parameters(): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # domain parameters + w = 12.0 + h = 6.0 + dx = 0.2 + dy = 0.1 + + solver.initialize_domain(w=w, h=h, dx=dx, dy=dy) + + # physical parameters + d = 5.0 + T_cold = 300.0 + T_hot = 600.0 + solver.initialize_physical_parameters(d=d, T_cold=T_cold, T_hot=T_hot) + + # Manually compute dt + # dx^2=0.04, dy^2=0.01 => sum=0.05 + # => numerator=0.04*0.01=0.0004 + # => denominator=2 * 5.0 * 0.05= 0.5 + # => dt= 0.0004/0.5= 0.0008 + dt_expected = 0.0008 + +> assert np.isclose(solver.dt, dt_expected, rtol=1e-12), \ + f"dt expected {dt_expected}, got {solver.dt}" +E AssertionError: dt expected 0.0008, got 0.0005333333333333335 +E assert np.False_ +E + where np.False_ = (0.0005333333333333335, 0.0008, rtol=1e-12) +E + where = np.isclose +E + and 0.0005333333333333335 = .dt + +tests/integration/test_diffusion2d.py:34: AssertionError +------------------------------------------------------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------------------------------------------------- +dt = 0.0005333333333333335 +__________________________________________________________________________________________________________ test_set_initial_condition __________________________________________________________________________________________________________ + + def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + + # domain parameters + w = 10.0 + h = 10.0 + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + # => solver.nx = 20, solver.ny = 20 + + # physical parameters + solver.initialize_physical_parameters(d=4.0, T_cold=200.0, T_hot=400.0) + + # Now we manually compute the expected array + nx = solver.nx # 20 + ny = solver.ny # 20 + r = 2.0 + cx = 5.0 + cy = 5.0 + + # Initialize everything to T_cold + u_expected = np.full((nx, ny), solver.T_cold) + + # For each grid point (i, j), check if it's inside the circle + # Circle of radius 2.0, centered at (cx, cy) = (5.0, 5.0) + # x-coordinate of index i: x_i = i * dx + # y-coordinate of index j: y_j = j * dy + for i in range(nx): + for j in range(ny): + xcoord = i * dx + ycoord = j * dy + distance_sq = (xcoord - cx)**2 + (ycoord - cy)**2 + if distance_sq < r**2: + u_expected[i, j] = solver.T_hot + + # Get the actual result from the solver + u_computed = solver.set_initial_condition() + + # Compare the entire array + assert u_computed.shape == u_expected.shape, ( + f"Shape mismatch: expected {u_expected.shape}, got {u_computed.shape}" + ) + + # Use np.allclose for a floating-point safe comparison of all elements +> assert np.allclose(u_computed, u_expected), ( + "Computed 2D array does not match expected 2D array for set_initial_condition" + ) +E AssertionError: Computed 2D array does not match expected 2D array for set_initial_condition +E assert False +E + where False = (array([[400., 400., 400., 400., 400., 400., 400., 400., 400., 400., 400.,\n 400., 400., 400., 400., 400., 400., ..., 400., 400., 400., 400., 400., 400., 400., 400., 400.,\n 400., 400., 400., 400., 400., 400., 400., 400., 400.]]), array([[200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,\n 200., 200., 200., 200., 200., 200., ..., 200., 200., 200., 200., 200., 200., 200., 200., 200.,\n 200., 200., 200., 200., 200., 200., 200., 200., 200.]])) +E + where = np.allclose + +tests/integration/test_diffusion2d.py:87: AssertionError +------------------------------------------------------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------------------------------------------------- +dt = 0.010416666666666666 +=========================================================================================================== short test summary info ============================================================================================================ +FAILED tests/integration/test_diffusion2d.py::test_initialize_physical_parameters - AssertionError: dt expected 0.0008, got 0.0005333333333333335 +FAILED tests/integration/test_diffusion2d.py::test_set_initial_condition - AssertionError: Computed 2D array does not match expected 2D array for set_initial_condition +============================================================================================================== 2 failed in 0.43s =============================================================================================================== + + +``` + ### unittest log +```text +(venv) yonatan@Yonatans-MacBook-Pro testing-python-exercise-wt2425 % python -m unittest tests/unit/test_diffusion2d_functions.py +Fdt = 0.00016666666666666672 +FF +====================================================================== +FAIL: test_initialize_domain (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_initialize_domain) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/yonatan/Documents/SSE/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 30, in test_initialize_domain + self.assertEqual( +AssertionError: 50 != 60 : nx should be 60 but got 50 + +====================================================================== +FAIL: test_initialize_physical_parameters (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_initialize_physical_parameters) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/yonatan/Documents/SSE/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 61, in test_initialize_physical_parameters + self.assertEqual(self.solver.D, d, f"D should be {d} but got {self.solver.D}") +AssertionError: 15.0 != 5.0 : D should be 5.0 but got 15.0 + +====================================================================== +FAIL: test_set_initial_condition (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_set_initial_condition) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/Users/yonatan/Documents/SSE/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 90, in test_set_initial_condition + self.assertTrue( +AssertionError: np.False_ is not true : Center of hot disc should be T_hot. + +---------------------------------------------------------------------- +Ran 3 tests in 0.004s + +FAILED (failures=3) +``` + ## Citing The code used in this exercise is based on [Chapter 7 of the book "Learning Scientific Programming with Python"](https://scipython.com/book/chapter-7-matplotlib/examples/the-two-dimensional-diffusion-equation/). diff --git a/coverage-report.pdf b/coverage-report.pdf new file mode 100644 index 00000000..7d4e8638 Binary files /dev/null and b/coverage-report.pdf differ diff --git a/diffusion2d.py b/diffusion2d.py index 51a07f2d..541508b2 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -37,7 +37,12 @@ def __init__(self): # Timestep self.dt = None - def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): + def initialize_domain(self, w=10.0, h=10.0, dx=0.1, dy=0.1): + assert isinstance(w, float), "w must be a float" + assert isinstance(h, float), "h must be a float" + assert isinstance(dx, float), "dx must be a float" + assert isinstance(dy, float), "dy must be a float" + self.w = w self.h = h self.dx = dx @@ -45,7 +50,11 @@ def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): self.nx = int(w / dx) self.ny = int(h / dy) - def initialize_physical_parameters(self, d=4., T_cold=300, T_hot=700): + def initialize_physical_parameters(self, d=4.0, T_cold=300.0, T_hot=700.0): + assert isinstance(d, float), "d must be a float" + assert isinstance(T_cold, float), "T_cold must be a float" + assert isinstance(T_hot, float), "T_hot must be a float" + self.D = d self.T_cold = T_cold self.T_hot = T_hot @@ -61,7 +70,7 @@ def set_initial_condition(self): # Initial conditions - circle of radius r centred at (cx,cy) (mm) r, cx, cy = 2, 5, 5 - r2 = r ** 2 + r2 = r**2 for i in range(self.nx): for j in range(self.ny): p2 = (i * self.dx - cx) ** 2 + (j * self.dy - cy) ** 2 @@ -78,17 +87,20 @@ def do_timestep(self, u_nm1): # Propagate with forward-difference in time, central-difference in space u[1:-1, 1:-1] = u_nm1[1:-1, 1:-1] + self.D * self.dt * ( - (u_nm1[2:, 1:-1] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[:-2, 1:-1]) / dx2 - + (u_nm1[1:-1, 2:] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[1:-1, :-2]) / dy2) + (u_nm1[2:, 1:-1] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[:-2, 1:-1]) / dx2 + + (u_nm1[1:-1, 2:] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[1:-1, :-2]) / dy2 + ) return u.copy() def create_figure(self, fig, u, n, fignum): fignum += 1 ax = fig.add_subplot(220 + fignum) - im = ax.imshow(u.copy(), cmap=plt.get_cmap('hot'), vmin=self.T_cold, vmax=self.T_hot) + im = ax.imshow( + u.copy(), cmap=plt.get_cmap("hot"), vmin=self.T_cold, vmax=self.T_hot + ) ax.set_axis_off() - ax.set_title('{:.1f} ms'.format(n * self.dt * 1000)) + ax.set_title("{:.1f} ms".format(n * self.dt * 1000)) return fignum, im @@ -96,7 +108,7 @@ def create_figure(self, fig, u, n, fignum): def output_figure(fig, im): fig.subplots_adjust(right=0.85) cbar_ax = fig.add_axes([0.9, 0.15, 0.03, 0.7]) - cbar_ax.set_xlabel('$T$ / K', labelpad=20) + cbar_ax.set_xlabel("$T$ / K", labelpad=20) fig.colorbar(im, cax=cbar_ax) plt.show() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..eb32f4a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pytest +numpy +matplotlib +coverage diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index fd026b40..2722da7b 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -3,6 +3,7 @@ """ from diffusion2d import SolveDiffusion2D +import numpy as np def test_initialize_physical_parameters(): @@ -10,6 +11,30 @@ def test_initialize_physical_parameters(): Checks function SolveDiffusion2D.initialize_domain """ solver = SolveDiffusion2D() + # domain parameters + w = 12.0 + h = 6.0 + dx = 0.2 + dy = 0.1 + + solver.initialize_domain(w=w, h=h, dx=dx, dy=dy) + + # physical parameters + d = 5.0 + T_cold = 300.0 + T_hot = 600.0 + solver.initialize_physical_parameters(d=d, T_cold=T_cold, T_hot=T_hot) + + # Manually compute dt + # dx^2=0.04, dy^2=0.01 => sum=0.05 + # => numerator=0.04*0.01=0.0004 + # => denominator=2 * 5.0 * 0.05= 0.5 + # => dt= 0.0004/0.5= 0.0008 + dt_expected = 0.0008 + + assert np.isclose( + solver.dt, dt_expected, rtol=1e-12 + ), f"dt expected {dt_expected}, got {solver.dt}" def test_set_initial_condition(): @@ -17,3 +42,49 @@ def test_set_initial_condition(): Checks function SolveDiffusion2D.get_initial_function """ solver = SolveDiffusion2D() + + # domain parameters + w = 10.0 + h = 10.0 + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + # => solver.nx = 20, solver.ny = 20 + + # physical parameters + solver.initialize_physical_parameters(d=4.0, T_cold=200.0, T_hot=400.0) + + # Now we manually compute the expected array + nx = solver.nx # 20 + ny = solver.ny # 20 + r = 2.0 + cx = 5.0 + cy = 5.0 + + # Initialize everything to T_cold + u_expected = np.full((nx, ny), solver.T_cold) + + # For each grid point (i, j), check if it's inside the circle + # Circle of radius 2.0, centered at (cx, cy) = (5.0, 5.0) + # x-coordinate of index i: x_i = i * dx + # y-coordinate of index j: y_j = j * dy + for i in range(nx): + for j in range(ny): + xcoord = i * dx + ycoord = j * dy + distance_sq = (xcoord - cx) ** 2 + (ycoord - cy) ** 2 + if distance_sq < r**2: + u_expected[i, j] = solver.T_hot + + # Get the actual result from the solver + u_computed = solver.set_initial_condition() + + # Compare the entire array + assert ( + u_computed.shape == u_expected.shape + ), f"Shape mismatch: expected {u_expected.shape}, got {u_computed.shape}" + + # Use np.allclose for a floating-point safe comparison of all elements + assert np.allclose( + u_computed, u_expected + ), "Computed 2D array does not match expected 2D array for set_initial_condition" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index c4277ffd..1fe65a7b 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -2,25 +2,105 @@ Tests for functions in class SolveDiffusion2D """ +import unittest +import numpy as np from diffusion2d import SolveDiffusion2D -def test_initialize_domain(): - """ - Check function SolveDiffusion2D.initialize_domain - """ - solver = SolveDiffusion2D() +class TestDiffusion2D(unittest.TestCase): + def setUp(self): + self.solver = SolveDiffusion2D() + def test_initialize_domain(self): + # Manually pick domain parameters + w = 12.0 + h = 10.0 + dx = 0.2 + dy = 0.1 -def test_initialize_physical_parameters(): - """ - Checks function SolveDiffusion2D.initialize_domain - """ - solver = SolveDiffusion2D() + # Expected nx, ny + nx_expected = int(w / dx) # 12 / 0.2 = 60 + ny_expected = int(h / dy) # 10 / 0.1 = 100 + # Call the function + self.solver.initialize_domain(w=w, h=h, dx=dx, dy=dy) -def test_set_initial_condition(): - """ - Checks function SolveDiffusion2D.get_initial_function - """ - solver = SolveDiffusion2D() + # Check using unittest's self.assertEqual + self.assertEqual( + self.solver.nx, + nx_expected, + f"nx should be {nx_expected} but got {self.solver.nx}", + ) + self.assertEqual( + self.solver.ny, + ny_expected, + f"ny should be {ny_expected} but got {self.solver.ny}", + ) + + def test_initialize_physical_parameters(self): + # We must define dx, dy first + self.solver.dx = 0.1 + self.solver.dy = 0.1 + + d = 5.0 + T_cold = 300.0 + T_hot = 600.0 + + # Manually compute dt + # dx^2=0.01, dy^2=0.01 => sum=0.02 + # => numerator=1e-4 + # => denominator=2*5.0*0.02=0.2 + # => dt=1e-4/0.2=5.e-4=0.0005 + dt_expected = 0.0005 + + # Call the function + self.solver.initialize_physical_parameters(d=d, T_cold=T_cold, T_hot=T_hot) + + # Check + self.assertEqual(self.solver.D, d, f"D should be {d} but got {self.solver.D}") + self.assertEqual( + self.solver.T_cold, + T_cold, + f"T_cold should be {T_cold} but got {self.solver.T_cold}", + ) + self.assertEqual( + self.solver.T_hot, + T_hot, + f"T_hot should be {T_hot} but got {self.solver.T_hot}", + ) + self.assertTrue( + np.isclose(self.solver.dt, dt_expected, rtol=1e-12), + f"dt should be {dt_expected}, got {self.solver.dt}", + ) + + def test_set_initial_condition(self): + self.solver.nx = 100 + self.solver.ny = 200 + self.solver.dx = 0.1 + self.solver.dy = 0.1 + self.solver.T_cold = 300.0 + self.solver.T_hot = 700.0 + + # Call the function + u = self.solver.set_initial_condition() + + # Check shape + self.assertEqual( + u.shape, (100, 200), f"Expected array shape (100, 200), got {u.shape}" + ) + + # The center is index [50, 50] + self.assertTrue( + np.isclose(u[50, 50], self.solver.T_hot), + "Center of hot disc should be T_hot.", + ) + + # Outside circle at [0, 0] + self.assertTrue( + np.isclose(u[0, 0], self.solver.T_cold), + "Point (0,0) should remain at T_cold.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tox.toml b/tox.toml new file mode 100644 index 00000000..6c0d7ec8 --- /dev/null +++ b/tox.toml @@ -0,0 +1,17 @@ +env_list = ["format", "3.11"] + +[env.format] +deps = ["black==22.3.0"] +skip_install = true +commands = [ + ["black", "."] +] + +[env."3.11"] +deps = [ + "-rrequirements.txt", +] +commands = [ + "python -m unittest tests/unit/test_diffusion2d_functions.py && python -m pytest tests/integration/test_diffusion2d.py {posargs}" +] +