Repository: skilkis/numfoil

Issue:
Merge vector2d module into geom2d module
Currently the `vector2d` and `geom2d` modules are awkward to import even though they serve similar purposes. Merging these two will make the vector geometry more user-friendly.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=h1) Report
> Merging [#16](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=desc) (46ab52c) into [master](https://codecov.io/gh/skilkis/numfoil/commit/dd8ba85ae3b2009aa3dafa47a6247fea08432720?el=desc) (dd8ba85) will **decrease** coverage by `0.06%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/skilkis/numfoil/pull/16/graphs/tree.svg?width=650&height=150&src=pr&token=V8I4W4GDBS)](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #16      +/-   ##
==========================================
- Coverage   76.95%   76.88%   -0.07%     
==========================================
  Files          13       12       -1     
  Lines         677      675       -2     
==========================================
- Hits          521      519       -2     
  Misses        156      156              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `76.88% <100.00%> (-0.07%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/numfoil/geometry/\_\_init\_\_.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvZ2VvbWV0cnkvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [src/numfoil/geometry/airfoil.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvZ2VvbWV0cnkvYWlyZm9pbC5weQ==) | `90.62% <100.00%> (ø)` | |
| [src/numfoil/geometry/geom2d.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvZ2VvbWV0cnkvZ2VvbTJkLnB5) | `100.00% <100.00%> (ø)` | |
| [src/numfoil/geometry/panel.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvZ2VvbWV0cnkvcGFuZWwucHk=) | `100.00% <100.00%> (ø)` | |
| [src/numfoil/geometry/spline.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvZ2VvbWV0cnkvc3BsaW5lLnB5) | `100.00% <100.00%> (ø)` | |
| [src/numfoil/solver/base.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvc29sdmVyL2Jhc2UucHk=) | `56.46% <100.00%> (-0.59%)` | :arrow_down: |
| [src/numfoil/solver/m\_constant\_vortex.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvc29sdmVyL21fY29uc3RhbnRfdm9ydGV4LnB5) | `55.55% <100.00%> (ø)` | |
| [src/numfoil/solver/m\_linear\_vortex.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvc29sdmVyL21fbGluZWFyX3ZvcnRleC5weQ==) | `82.27% <100.00%> (ø)` | |
| [src/numfoil/solver/m\_lumped\_vortex.py](https://codecov.io/gh/skilkis/numfoil/pull/16/diff?src=pr&el=tree#diff-c3JjL251bWZvaWwvc29sdmVyL21fbHVtcGVkX3ZvcnRleC5weQ==) | `74.50% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=footer). Last update [dd8ba85...46ab52c](https://codecov.io/gh/skilkis/numfoil/pull/16?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

skilkis: @kswannet The codecov issue is fixed now. Take a look and approve the changes when you have time 😄 

Patch:
diff --git a/src/numfoil/geometry/__init__.py b/src/numfoil/geometry/__init__.py
index 8a9a68c..a6b2a31 100644
--- a/src/numfoil/geometry/__init__.py
+++ b/src/numfoil/geometry/__init__.py
@@ -1,3 +1,25 @@
 from .airfoil import Airfoil, NACA4Airfoil, UIUCAirfoil
+from .geom2d import (
+    Point2D,
+    Vector2D,
+    is_row_vector,
+    magnitude_2d,
+    normalize_2d,
+    rotate_2d_90ccw,
+)
+from .panel import Panel2D
+from .spline import BSpline2D
 
-__all__ = ["Airfoil", "NACA4Airfoil", "UIUCAirfoil"]
+__all__ = [
+    "Airfoil",
+    "BSpline2D",
+    "NACA4Airfoil",
+    "Panel2D",
+    "Point2D",
+    "UIUCAirfoil",
+    "Vector2D",
+    "is_row_vector",
+    "magnitude_2d",
+    "normalize_2d",
+    "rotate_2d_90ccw",
+]
diff --git a/src/numfoil/geometry/airfoil.py b/src/numfoil/geometry/airfoil.py
index 38f8659..d0a93c3 100644
--- a/src/numfoil/geometry/airfoil.py
+++ b/src/numfoil/geometry/airfoil.py
@@ -23,12 +23,10 @@ import matplotlib
 import numpy as np
 from matplotlib import pyplot as plt
 
+from .geom2d import normalize_2d, rotate_2d_90ccw
 from .spline import BSpline2D
-from .vector2d import normalize_2d, rotate_2d_90ccw
 
 # TODO Add NACA5 series Airfoil as a fun nice-to-have feature
-# TODO Add the ability to import an arbitrary airfoil from a data-file
-# TODO use bezier package on PyPi to have camber-line calculations
 
 AIRFOIL_REPR_REGEX = re.compile(r"[.]([A-Z])\w+")
 
diff --git a/src/numfoil/geometry/geom2d.py b/src/numfoil/geometry/geom2d.py
index 984a55d..4a3ea2b 100644
--- a/src/numfoil/geometry/geom2d.py
+++ b/src/numfoil/geometry/geom2d.py
@@ -12,13 +12,22 @@
 # implied. See the License for the specific language governing
 # permissions and limitations under the License.
 
-"""Contains primitive geometric definitions for points and vectors."""
+"""Contains definitions for 2D points, vectors, and transforms."""
+
+from __future__ import annotations
 
 from typing import Sequence, Tuple, Union
 
 import numpy as np
 
-from .vector2d import is_row_vector, magnitude_2d, normalize_2d
+__all__ = [
+    "Point2D",
+    "Vector2D",
+    "is_row_vector",
+    "magnitude_2d",
+    "normalize_2d",
+    "rotate_2d_90ccw",
+]
 
 
 class Geom2D(np.ndarray):
@@ -48,7 +57,7 @@ class Geom2D(np.ndarray):
 class Point2D(Geom2D):
     """Defines a point in 2D space."""
 
-    def __sub__(self, other) -> object:
+    def __sub__(self, other) -> Vector2D:
         """Overloads subtract magic method to allow vector creation.
 
         When two :py:class:`Point2D` objects are subtracted from
@@ -89,3 +98,67 @@ class Vector2D(Geom2D):
     def normalized(self) -> np.ndarray:
         """Returns the unit-vector(s) of :py:class:`Vector2D`."""
         return normalize_2d(self)
+
+
+R_90CCW_MATRIX = np.array([[0, 1], [-1, 0]])
+
+
+def rotate_2d_90ccw(array: np.ndarray, inplace: bool = False) -> np.ndarray:
+    """Rotates 2D row-vector(s) 90 degrees counter-clockwise.
+
+    Args:
+        array: An array of row-vector(s) with shape (n, 2)
+        inplace: If the normalization should be performed inplace
+            on the ``array`` object.
+
+    Returns:
+        Transformed row-vector(s) with dimension (n, 2).
+    """
+    assert is_row_vector(array)
+    return np.dot(a=array, b=R_90CCW_MATRIX, out=array if inplace else None)
+
+
+def magnitude_2d(array: np.ndarray) -> np.ndarray:
+    """Calculates the magnitude of 2D row-vector(s).
+
+    Args:
+        array: An array of row-vector(s) with shape (n, 2)
+
+    Returns:
+        A column-vector of size (n, 1) containing n magnitudes.
+    """
+    assert is_row_vector(array)
+    if len(array.shape) < 2:  # array is a 1D vector
+        return np.linalg.norm(array).reshape(1, 1)
+    else:
+        return np.linalg.norm(array, axis=1).reshape((array.shape[0], 1))
+
+
+def normalize_2d(array: np.ndarray, inplace: bool = False) -> np.ndarray:
+    """Normalizes 2D row-vector(s) into unit-vector(s).
+
+    Args:
+        array: An array of row-vector(s) with shape (n, 2)
+        inplace: If the normalization should be performed inplace
+            on the ``array`` object.
+
+    Returns:
+        Normalized row-vector(s) with dimension (n, 2).
+    """
+    assert is_row_vector(array)
+    return np.divide(
+        array, magnitude_2d(array), out=array if inplace else None,
+    )
+
+
+def is_row_vector(array: np.ndarray) -> bool:
+    """Returns ``True`` if ``array`` is contains 2D row-vector(s).
+
+    Raises:
+        ValueError: If an ``array`` is 3D or contains column-vector(s)
+    """
+    if len(array.shape) == 2 and array.shape[1] != 2:
+        raise ValueError(
+            "The input `array` must contain 2D row-vectors with shape (n, 2)"
+        )
+    return True
diff --git a/src/numfoil/geometry/panel.py b/src/numfoil/geometry/panel.py
index 10c095c..752d134 100644
--- a/src/numfoil/geometry/panel.py
+++ b/src/numfoil/geometry/panel.py
@@ -20,8 +20,7 @@ import matplotlib
 import numpy as np
 from matplotlib import pyplot as plt
 
-from .geom2d import Geom2D, Point2D, Vector2D
-from .vector2d import rotate_2d_90ccw
+from .geom2d import Geom2D, Point2D, Vector2D, rotate_2d_90ccw
 
 
 class Panel2D(Geom2D):
diff --git a/src/numfoil/geometry/spline.py b/src/numfoil/geometry/spline.py
index ef5d08f..b9a872e 100644
--- a/src/numfoil/geometry/spline.py
+++ b/src/numfoil/geometry/spline.py
@@ -6,7 +6,7 @@ from typing import Optional, Union
 import numpy as np
 import scipy.interpolate as si
 
-from .vector2d import normalize_2d, rotate_2d_90ccw
+from .geom2d import normalize_2d, rotate_2d_90ccw
 
 
 class BSpline2D:
diff --git a/src/numfoil/geometry/vector2d.py b/src/numfoil/geometry/vector2d.py
deleted file mode 100644
index 49b3eb6..0000000
--- a/src/numfoil/geometry/vector2d.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Copyright 2020 Kilian Swannet, San Kilkis
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied. See the License for the specific language governing
-# permissions and limitations under the License.
-
-
-"""Contains functions for performing operations on 2D vectors.
-
-A 2D row-vector is defined as one with 1 row and 2 columns. This
-function can also handle a set of n row-vectors given as a 2D
-array with n rows and 2 columns. The transform will be applied to
-each vector individually using the inner matrix (dot) product.
-"""
-
-
-import numpy as np
-
-__all__ = ["rotate_2d_90ccw", "magnitude_2d", "normalize_2d"]
-
-
-R_90CCW_MATRIX = np.array([[0, 1], [-1, 0]])
-
-
-def rotate_2d_90ccw(array: np.ndarray, inplace: bool = False) -> np.ndarray:
-    """Rotates 2D row-vector(s) 90 degrees counter-clockwise.
-
-    Args:
-        array: An array of row-vector(s) with shape (n, 2)
-        inplace: If the normalization should be performed inplace
-            on the ``array`` object.
-
-    Returns:
-        Transformed row-vector(s) with dimension (n, 2).
-    """
-    assert is_row_vector(array)
-    return np.dot(a=array, b=R_90CCW_MATRIX, out=array if inplace else None)
-
-
-def magnitude_2d(array: np.ndarray) -> np.ndarray:
-    """Calculates the magnitude of 2D row-vector(s).
-
-    Args:
-        array: An array of row-vector(s) with shape (n, 2)
-
-    Returns:
-        A column-vector of size (n, 1) containing n magnitudes.
-    """
-    assert is_row_vector(array)
-    if len(array.shape) < 2:  # array is a 1D vector
-        return np.linalg.norm(array).reshape(1, 1)
-    else:
-        return np.linalg.norm(array, axis=1).reshape((array.shape[0], 1))
-
-
-def normalize_2d(array: np.ndarray, inplace: bool = False) -> np.ndarray:
-    """Normalizes 2D row-vector(s) into unit-vector(s).
-
-    Args:
-        array: An array of row-vector(s) with shape (n, 2)
-        inplace: If the normalization should be performed inplace
-            on the ``array`` object.
-
-    Returns:
-        Normalized row-vector(s) with dimension (n, 2).
-    """
-    assert is_row_vector(array)
-    return np.divide(
-        array, magnitude_2d(array), out=array if inplace else None,
-    )
-
-
-def is_row_vector(array: np.ndarray) -> bool:
-    """Returns ``True`` if ``array`` is contains 2D row-vector(s).
-
-    Raises:
-        ValueError: If an ``array`` is 3D or contains column-vector(s)
-    """
-    if len(array.shape) == 2 and array.shape[1] != 2:
-        raise ValueError(
-            "The input `array` must contain 2D row-vectors with shape (n, 2)"
-        )
-    return True
diff --git a/src/numfoil/solver/base.py b/src/numfoil/solver/base.py
index 74ec60d..25bec21 100644
--- a/src/numfoil/solver/base.py
+++ b/src/numfoil/solver/base.py
@@ -23,10 +23,7 @@ from typing import Generator, Optional, Sequence, Tuple, Type, Union
 import numpy as np
 from matplotlib import pyplot as plt
 
-from numfoil.geometry import Airfoil
-from numfoil.geometry.geom2d import Vector2D
-from numfoil.geometry.panel import Panel2D
-from numfoil.geometry.vector2d import rotate_2d_90ccw
+from numfoil.geometry import Airfoil, Panel2D, Vector2D, rotate_2d_90ccw
 
 FAST_MATH_FLAGS = {
     # Refer to https://llvm.org/docs/LangRef.html#fast-math-flags
@@ -89,19 +86,16 @@ class FlowSolution(metaclass=ABCMeta):
 
         Explicitely this is DeltaCp = Cp_lower - Cp_upper.
         """
-        ...
 
     @property
     @abstractmethod
     def pressure_coefficients(self) -> np.ndarray:
         """Pressure coefficient distribution across all panels."""
-        ...
 
     @property
     @abstractmethod
     def lift_coefficient(self) -> Union[float, np.ndarray]:
         """Lift coefficient generated by the pressure distribution."""
-        ...
 
     def enumerate_alpha(
         self, alpha: Optional[float] = None
@@ -337,13 +331,11 @@ class PanelMethod(metaclass=ABCMeta):
     @abstractmethod
     def panels(self) -> Panel2D:
         """Returns the panelled representation of :py:attr:`airfoil."""
-        ...
 
     @property
     @abstractmethod
     def collocation_points(self) -> np.ndarray:
         """Returns the collocation points used in the solution."""
-        ...
 
     @property
     @abstractmethod
@@ -358,13 +350,11 @@ class PanelMethod(metaclass=ABCMeta):
         coefficients purely describe the geometric coupling of the
         panels and is independent of the flow condition.
         """
-        ...
 
     @property
     @abstractmethod
     def unit_rhs_vector(self) -> np.ndarray:
         """."""
-        ...
 
     @property
     def solution_class(self) -> Type[FlowSolution]:
diff --git a/src/numfoil/solver/m_constant_vortex.py b/src/numfoil/solver/m_constant_vortex.py
index 13fdc8b..25df152 100644
--- a/src/numfoil/solver/m_constant_vortex.py
+++ b/src/numfoil/solver/m_constant_vortex.py
@@ -21,7 +21,7 @@ from typing import Dict, Sequence, Tuple, Union
 import numba
 import numpy as np
 
-from numfoil.geometry.panel import Panel2D
+from numfoil.geometry import Panel2D
 from numfoil.solver.base import BASE_NUMBA_CONFIG, PanelMethod
 
 # Great source explaining it
diff --git a/src/numfoil/solver/m_linear_vortex.py b/src/numfoil/solver/m_linear_vortex.py
index 820e435..2ec9414 100644
--- a/src/numfoil/solver/m_linear_vortex.py
+++ b/src/numfoil/solver/m_linear_vortex.py
@@ -21,7 +21,7 @@ from typing import Dict, Sequence, Tuple, Union
 import numba
 import numpy as np
 
-from numfoil.geometry.panel import Panel2D
+from numfoil.geometry import Panel2D
 from numfoil.solver.base import (
     BASE_NUMBA_CONFIG,
     PanelMethod,
diff --git a/src/numfoil/solver/m_lumped_vortex.py b/src/numfoil/solver/m_lumped_vortex.py
index 49e39c0..d13670d 100644
--- a/src/numfoil/solver/m_lumped_vortex.py
+++ b/src/numfoil/solver/m_lumped_vortex.py
@@ -21,7 +21,7 @@ from typing import Type
 import numba
 import numpy as np
 
-from numfoil.geometry.panel import Panel2D
+from numfoil.geometry import Panel2D
 from numfoil.solver.base import (
     BASE_NUMBA_CONFIG,
     PanelMethod,


Test patch:
diff --git a/tests/test_numfoil/test_geometry/test_vector2d.py b/tests/test_numfoil/test_geometry/test_geom2d.py
similarity index 99%
rename from tests/test_numfoil/test_geometry/test_vector2d.py
rename to tests/test_numfoil/test_geometry/test_geom2d.py
index 641f179..746618e 100644
--- a/tests/test_numfoil/test_geometry/test_vector2d.py
+++ b/tests/test_numfoil/test_geometry/test_geom2d.py
@@ -15,7 +15,7 @@
 import numpy as np
 import pytest
 
-from numfoil.geometry.vector2d import (
+from numfoil.geometry import (
     is_row_vector,
     magnitude_2d,
     normalize_2d,
diff --git a/tests/test_numfoil/test_geometry/test_panel.py b/tests/test_numfoil/test_geometry/test_panel.py
index 554bae1..17b098a 100644
--- a/tests/test_numfoil/test_geometry/test_panel.py
+++ b/tests/test_numfoil/test_geometry/test_panel.py
@@ -16,8 +16,7 @@ import numpy as np
 import pytest
 from matplotlib.quiver import Quiver
 
-from numfoil.geometry.geom2d import Point2D, Vector2D
-from numfoil.geometry.panel import Panel2D
+from numfoil.geometry import Panel2D, Point2D, Vector2D
 from tests.test_numfoil.helpers import ScenarioTestSuite
 
 
diff --git a/tests/test_numfoil/test_geometry/test_spline.py b/tests/test_numfoil/test_geometry/test_spline.py
index 9afa0a4..10c188c 100644
--- a/tests/test_numfoil/test_geometry/test_spline.py
+++ b/tests/test_numfoil/test_geometry/test_spline.py
@@ -14,7 +14,7 @@
 
 import numpy as np
 
-from numfoil.geometry.spline import BSpline2D
+from numfoil.geometry import BSpline2D
 from tests.test_numfoil.helpers import ScenarioTestSuite
 
 
diff --git a/tests/test_numfoil/test_solver/test_m_linear_vortex.py b/tests/test_numfoil/test_solver/test_m_linear_vortex.py
index 02bb09a..e5953cf 100644
--- a/tests/test_numfoil/test_solver/test_m_linear_vortex.py
+++ b/tests/test_numfoil/test_solver/test_m_linear_vortex.py
@@ -17,8 +17,7 @@ from pathlib import Path
 import numpy as np
 import pytest
 
-from numfoil.geometry.airfoil import NACA4Airfoil
-from numfoil.geometry.panel import Panel2D
+from numfoil.geometry import NACA4Airfoil, Panel2D
 from numfoil.solver.base import PanelMethod
 from numfoil.solver.m_linear_vortex import (
     calc_integration_constants,