From 90799205a5c69214e5e813db2e819415a921948b Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 04:56:29 +0100 Subject: [PATCH 1/7] Add assertions --- diffusion2d.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/diffusion2d.py b/diffusion2d.py index 51a07f2d..7db10be7 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -38,6 +38,11 @@ def __init__(self): self.dt = None def initialize_domain(self, w=10., h=10., 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., T_cold=300., T_hot=700.): + 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 From 3a0b8eb2ef1dcce0a3d3c3f67064206c58cc6cf0 Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 05:29:56 +0100 Subject: [PATCH 2/7] Add unit tests (pytest) --- tests/unit/test_diffusion2d_functions.py | 68 +++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index c4277ffd..bb9b2898 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -3,13 +3,28 @@ """ from diffusion2d import SolveDiffusion2D - +import numpy as np 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}" + assert solver.ny == ny, f"ny should be {ny} but got {solver.ny}" + def test_initialize_physical_parameters(): @@ -18,9 +33,60 @@ def test_initialize_physical_parameters(): """ 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}" + assert solver.T_cold == T_cold, f"T_cold should be {T_cold} but got {solver.T_cold}" + assert solver.T_hot == T_hot, f"T_hot should be {T_hot} but got {solver.T_hot}" + assert np.isclose(solver.dt, dt_expected, rtol=1e-12), \ + f"dt should be {dt_expected} but got {solver.dt}" + + 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." + + # (0,0) should be outside the center circle + assert np.isclose(u[0, 0], solver.T_cold), \ + "Point (0,0) should remain at temperature T_cold." \ No newline at end of file From 512c8aeb3425f5e85e527300824a65fb5a666bc2 Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 05:51:54 +0100 Subject: [PATCH 3/7] Step 4 (unittest) --- tests/unit/test_diffusion2d_functions.py | 179 ++++++++++++----------- 1 file changed, 93 insertions(+), 86 deletions(-) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index bb9b2898..5d69b14f 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -2,91 +2,98 @@ Tests for functions in class SolveDiffusion2D """ -from diffusion2d import SolveDiffusion2D +import unittest import numpy as np +from diffusion2d import SolveDiffusion2D + -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}" - assert solver.ny == ny, f"ny should be {ny} but got {solver.ny}" - - - -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}" - assert solver.T_cold == T_cold, f"T_cold should be {T_cold} but got {solver.T_cold}" - assert solver.T_hot == T_hot, f"T_hot should be {T_hot} but got {solver.T_hot}" - assert np.isclose(solver.dt, dt_expected, rtol=1e-12), \ - f"dt should be {dt_expected} but got {solver.dt}" - - - -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." - - # (0,0) should be outside the center circle - assert np.isclose(u[0, 0], solver.T_cold), \ - "Point (0,0) should remain at temperature T_cold." \ No newline at end of file +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 + + # 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) + + # 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." + ) From 80d51e395cec86d247790824e41fb6d646baf067 Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 05:52:59 +0100 Subject: [PATCH 4/7] Step 5 (integration tests) --- tests/integration/test_diffusion2d.py | 72 ++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index fd026b40..4e3f7721 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -3,13 +3,37 @@ """ from diffusion2d import SolveDiffusion2D - +import numpy as np 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 +41,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" + ) From 756d75fadbfea9c51314529a176c26931fefb881 Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 07:12:24 +0100 Subject: [PATCH 5/7] All tasks done --- README.md | 265 +++++++++++++++++++++++ coverage-report.pdf | Bin 0 -> 51699 bytes diffusion2d.py | 19 +- requirements.txt | 4 + tests/__init__.py | 0 tests/integration/__init__.py | 0 tests/integration/test_diffusion2d.py | 25 ++- tests/unit/__init__.py | 0 tests/unit/test_diffusion2d_functions.py | 34 +-- tox.toml | 17 ++ 10 files changed, 331 insertions(+), 33 deletions(-) create mode 100644 coverage-report.pdf create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tox.toml 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 0000000000000000000000000000000000000000..7d4e8638511443bc07207f84fe0525ca05125209 GIT binary patch literal 51699 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 ArT_o{ literal 0 HcmV?d00001 diff --git a/diffusion2d.py b/diffusion2d.py index 7db10be7..541508b2 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -37,7 +37,7 @@ 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" @@ -50,7 +50,7 @@ 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" @@ -70,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 @@ -87,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 @@ -105,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 4e3f7721..2722da7b 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -5,6 +5,7 @@ from diffusion2d import SolveDiffusion2D import numpy as np + def test_initialize_physical_parameters(): """ Checks function SolveDiffusion2D.initialize_domain @@ -31,9 +32,9 @@ def test_initialize_physical_parameters(): # => 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}" - + assert np.isclose( + solver.dt, dt_expected, rtol=1e-12 + ), f"dt expected {dt_expected}, got {solver.dt}" def test_set_initial_condition(): @@ -54,8 +55,8 @@ def test_set_initial_condition(): 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 + nx = solver.nx # 20 + ny = solver.ny # 20 r = 2.0 cx = 5.0 cy = 5.0 @@ -71,7 +72,7 @@ def test_set_initial_condition(): for j in range(ny): xcoord = i * dx ycoord = j * dy - distance_sq = (xcoord - cx)**2 + (ycoord - cy)**2 + distance_sq = (xcoord - cx) ** 2 + (ycoord - cy) ** 2 if distance_sq < r**2: u_expected[i, j] = solver.T_hot @@ -79,11 +80,11 @@ def test_set_initial_condition(): 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}" - ) + 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" - ) + 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 5d69b14f..0c849a47 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -8,7 +8,6 @@ class TestDiffusion2D(unittest.TestCase): - def setUp(self): self.solver = SolveDiffusion2D() @@ -30,12 +29,12 @@ def test_initialize_domain(self): self.assertEqual( self.solver.nx, nx_expected, - f"nx should be {nx_expected} but got {self.solver.nx}" + 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}" + f"ny should be {ny_expected} but got {self.solver.ny}", ) def test_initialize_physical_parameters(self): @@ -59,13 +58,19 @@ def test_initialize_physical_parameters(self): # 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.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}" + f"dt should be {dt_expected}, got {self.solver.dt}", ) def test_set_initial_condition(self): @@ -81,19 +86,22 @@ def test_set_initial_condition(self): # Check shape self.assertEqual( - u.shape, - (100, 200), - f"Expected array shape (100, 200), got {u.shape}" + 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." + "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." + "Point (0,0) should remain at T_cold.", ) + self.assertEqual(1, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tox.toml b/tox.toml new file mode 100644 index 00000000..bf36a24b --- /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 = [ + "python3 -m unittest tests/unit/test_diffusion2d_functions.py && python3 -m pytest tests/integration/test_diffusion2d.py {posargs}" +] + From ddf4f632e9e9867a5bc8b0254f30452c7b0f9047 Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 07:13:26 +0100 Subject: [PATCH 6/7] Remove statement --- tests/unit/test_diffusion2d_functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index 0c849a47..1fe65a7b 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -100,7 +100,6 @@ def test_set_initial_condition(self): np.isclose(u[0, 0], self.solver.T_cold), "Point (0,0) should remain at T_cold.", ) - self.assertEqual(1, 2) if __name__ == "__main__": From a8ea1f1e3a248414d66900a5c13b6d17156de7ab Mon Sep 17 00:00:00 2001 From: YonatanGM Date: Wed, 22 Jan 2025 07:15:13 +0100 Subject: [PATCH 7/7] Modified toml --- tox.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.toml b/tox.toml index bf36a24b..6c0d7ec8 100644 --- a/tox.toml +++ b/tox.toml @@ -12,6 +12,6 @@ deps = [ "-rrequirements.txt", ] commands = [ - "python3 -m unittest tests/unit/test_diffusion2d_functions.py && python3 -m pytest tests/integration/test_diffusion2d.py {posargs}" + "python -m unittest tests/unit/test_diffusion2d_functions.py && python -m pytest tests/integration/test_diffusion2d.py {posargs}" ]