diff --git a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean2DSpace.kt b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean2DSpace.kt index 252e2fcd5..e8b1ce95b 100644 --- a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean2DSpace.kt +++ b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean2DSpace.kt @@ -12,14 +12,14 @@ import space.kscience.kmath.operations.invoke import kotlin.math.sqrt @OptIn(UnstableKMathAPI::class) -public interface Vector2D : Point, Vector{ +public interface Vector2D : Point, Vector { public val x: Double public val y: Double override val size: Int get() = 2 override operator fun get(index: Int): Double = when (index) { - 1 -> x - 2 -> y + 0 -> x + 1 -> y else -> error("Accessing outside of point bounds") } @@ -27,7 +27,7 @@ public interface Vector2D : Point, Vector{ } public val Vector2D.r: Double - get() = Euclidean2DSpace { sqrt(norm()) } + get() = Euclidean2DSpace { norm() } @Suppress("FunctionName") public fun Vector2D(x: Double, y: Double): Vector2D = Vector2DImpl(x, y) diff --git a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean3DSpace.kt b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean3DSpace.kt index b0b2d2079..d567bee9a 100644 --- a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean3DSpace.kt +++ b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Euclidean3DSpace.kt @@ -19,9 +19,9 @@ public interface Vector3D : Point, Vector { override val size: Int get() = 3 override operator fun get(index: Int): Double = when (index) { - 1 -> x - 2 -> y - 3 -> z + 0 -> x + 1 -> y + 2 -> z else -> error("Accessing outside of point bounds") } @@ -31,7 +31,7 @@ public interface Vector3D : Point, Vector { @Suppress("FunctionName") public fun Vector3D(x: Double, y: Double, z: Double): Vector3D = Vector3DImpl(x, y, z) -public val Vector3D.r: Double get() = Euclidean3DSpace { sqrt(norm()) } +public val Vector3D.r: Double get() = Euclidean3DSpace { norm() } private data class Vector3DImpl( override val x: Double, diff --git a/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Projections.kt b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Projections.kt new file mode 100644 index 000000000..205bc17e7 --- /dev/null +++ b/kmath-geometry/src/commonMain/kotlin/space/kscience/kmath/geometry/Projections.kt @@ -0,0 +1,20 @@ +package space.kscience.kmath.geometry + +/** + * Project vector onto a line. + * @param vector to project + * @param line line to which vector should be projected + */ +public fun GeometrySpace.projectToLine(vector: V, line: Line): V = with(line) { + base + (direction dot (vector - base)) / (direction dot direction) * direction +} + +/** + * Project vector onto a hyperplane, which is defined by a normal and base. + * In 2D case it is the projection to a line, in 3d case it is the one to a plane. + * @param vector to project + * @param normal normal (perpendicular) vector to a hyper-plane to which vector should be projected + * @param base point belonging to a hyper-plane to which vector should be projected + */ +public fun GeometrySpace.projectAlong(vector: V, normal: V, base: V): V = + vector + normal * ((base - vector) dot normal) / (normal dot normal) diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean2DSpaceTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean2DSpaceTest.kt new file mode 100644 index 000000000..5913b2fa9 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean2DSpaceTest.kt @@ -0,0 +1,62 @@ +package space.kscience.kmath.geometry + +import kotlin.math.sqrt +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class Euclidean2DSpaceTest { + @Test + fun zero() { + assertVectorEquals(Vector2D(0.0, 0.0), Euclidean2DSpace.zero) + } + + @Test + fun norm() { + with(Euclidean2DSpace) { + assertEquals(0.0, zero.norm()) + assertEquals(1.0, Vector2D(1.0, 0.0).norm()) + assertEquals(sqrt(2.0), Vector2D(1.0, 1.0).norm()) + assertEquals(sqrt(5.002001), Vector2D(-2.0, 1.001).norm()) + } + } + + @Test + fun dotProduct() { + with(Euclidean2DSpace) { + assertEquals(0.0, zero dot zero) + assertEquals(0.0, zero dot Vector2D(1.0, 0.0)) + assertEquals(0.0, Vector2D(-2.0, 0.001) dot zero) + assertEquals(0.0, Vector2D(1.0, 0.0) dot Vector2D(0.0, 1.0)) + + assertEquals(1.0, Vector2D(1.0, 0.0) dot Vector2D(1.0, 0.0)) + assertEquals(-2.0, Vector2D(0.0, 1.0) dot Vector2D(1.0, -2.0)) + assertEquals(2.0, Vector2D(1.0, 1.0) dot Vector2D(1.0, 1.0)) + assertEquals(4.001001, Vector2D(-2.0, 1.001) dot Vector2D(-2.0, 0.001)) + + assertEquals(-4.998, Vector2D(1.0, 2.0) dot Vector2D(-5.0, 0.001)) + } + } + + @Test + fun add() { + with(Euclidean2DSpace) { + assertVectorEquals( + Vector2D(-2.0, 0.001), + Vector2D(-2.0, 0.001) + zero + ) + assertVectorEquals( + Vector2D(-3.0, 3.001), + Vector2D(2.0, 3.0) + Vector2D(-5.0, 0.001) + ) + } + } + + @Test + fun multiply() { + with(Euclidean2DSpace) { + assertVectorEquals(Vector2D(-4.0, 0.0), Vector2D(-2.0, 0.0) * 2) + assertVectorEquals(Vector2D(4.0, 0.0), Vector2D(-2.0, 0.0) * -2) + assertVectorEquals(Vector2D(300.0, 0.0003), Vector2D(100.0, 0.0001) * 3) + } + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean3DSpaceTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean3DSpaceTest.kt new file mode 100644 index 000000000..2c74cbd27 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Euclidean3DSpaceTest.kt @@ -0,0 +1,74 @@ +package space.kscience.kmath.geometry + +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class Euclidean3DSpaceTest { + @Test + fun zero() { + assertVectorEquals(Vector3D(0.0, 0.0, 0.0), Euclidean3DSpace.zero) + } + + @Test + fun distance() { + with(Euclidean3DSpace) { + assertEquals(0.0, zero.distanceTo(zero)) + assertEquals(1.0, zero.distanceTo(Vector3D(1.0, 0.0, 0.0))) + assertEquals(kotlin.math.sqrt(5.000001), Vector3D(1.0, -2.0, 0.001).distanceTo(zero)) + assertEquals(0.0, Vector3D(1.0, -2.0, 0.001).distanceTo(Vector3D(1.0, -2.0, 0.001))) + assertEquals(0.0, Vector3D(1.0, 0.0, 0.0).distanceTo(Vector3D(1.0, 0.0, 0.0))) + assertEquals(kotlin.math.sqrt(2.0), Vector3D(1.0, 0.0, 0.0).distanceTo(Vector3D(1.0, 1.0, 1.0))) + assertEquals(3.1622778182822584, Vector3D(0.0, 1.0, 0.0).distanceTo(Vector3D(1.0, -2.0, 0.001))) + assertEquals(0.0, Vector3D(1.0, -2.0, 0.001).distanceTo(Vector3D(1.0, -2.0, 0.001))) + assertEquals(9.695050335093676, Vector3D(1.0, 2.0, 3.0).distanceTo(Vector3D(7.0, -5.0, 0.001))) + } + } + + @Test + fun norm() { + with(Euclidean3DSpace) { + assertEquals(0.0, zero.norm()) + assertEquals(1.0, Vector3D(1.0, 0.0, 0.0).norm()) + assertEquals(kotlin.math.sqrt(3.0), Vector3D(1.0, 1.0, 1.0).norm()) + assertEquals(kotlin.math.sqrt(5.000001), Vector3D(1.0, -2.0, 0.001).norm()) + } + } + + @Test + fun dotProduct() { + with(Euclidean3DSpace) { + assertEquals(0.0, zero dot zero) + assertEquals(0.0, zero dot Vector3D(1.0, 0.0, 0.0)) + assertEquals(0.0, Vector3D(1.0, -2.0, 0.001) dot zero) + + assertEquals(1.0, Vector3D(1.0, 0.0, 0.0) dot Vector3D(1.0, 0.0, 0.0)) + assertEquals(1.0, Vector3D(1.0, 0.0, 0.0) dot Vector3D(1.0, 1.0, 1.0)) + assertEquals(-2.0, Vector3D(0.0, 1.0, 0.0) dot Vector3D(1.0, -2.0, 0.001)) + assertEquals(3.0, Vector3D(1.0, 1.0, 1.0) dot Vector3D(1.0, 1.0, 1.0)) + assertEquals(5.000001, Vector3D(1.0, -2.0, 0.001) dot Vector3D(1.0, -2.0, 0.001)) + + assertEquals(-2.997, Vector3D(1.0, 2.0, 3.0) dot Vector3D(7.0, -5.0, 0.001)) + } + } + + @Test + fun add() { + with(Euclidean3DSpace) { + assertVectorEquals( + Vector3D(1.0, -2.0, 0.001), + Vector3D(1.0, -2.0, 0.001) + zero + ) + assertVectorEquals( + Vector3D(8.0, -3.0, 3.001), + Vector3D(1.0, 2.0, 3.0) + Vector3D(7.0, -5.0, 0.001) + ) + } + } + + @Test + fun multiply() { + with(Euclidean3DSpace) { + assertVectorEquals(Vector3D(2.0, -4.0, 0.0), Vector3D(1.0, -2.0, 0.0) * 2) + } + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionAlongTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionAlongTest.kt new file mode 100644 index 000000000..55fc39aad --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionAlongTest.kt @@ -0,0 +1,61 @@ +package space.kscience.kmath.geometry + +import kotlin.test.Test +import kotlin.test.assertTrue + +internal class ProjectionAlongTest { + @Test + fun projectionIntoYEqualsX() { + with(Euclidean2DSpace) { + val normal = Vector2D(-2.0, 2.0) + val base = Vector2D(2.3, 2.3) + + assertVectorEquals(zero, projectAlong(zero, normal, base)) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + val d = (y - x) / 2.0 + assertVectorEquals(Vector2D(x + d, y - d), projectAlong(Vector2D(x, y), normal, base)) + } + } + } + + @Test + fun projectionOntoLine() { + with(Euclidean2DSpace) { + val a = 5.0 + val b = -3.0 + val c = -15.0 + val normal = Vector2D(-5.0, 3.0) + val base = Vector2D(3.0, 0.0) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + val xProj = (b * (b * x - a * y) - a * c) / (a * a + b * b) + val yProj = (a * (-b * x + a * y) - b * c) / (a * a + b * b) + assertVectorEquals(Vector2D(xProj, yProj), projectAlong(Vector2D(x, y), normal, base)) + } + } + } + + @Test + fun projectOntoPlane() { + val normal = Vector3D(1.0, 3.5, 0.07) + val base = Vector3D(2.0, -0.0037, 11.1111) + + with(Euclidean3DSpace) { + val testDomain = (-10.0..10.0).generateList(0.43) + for (x in testDomain) { + for (y in testDomain) { + for (z in testDomain) { + val v = Vector3D(x, y, z) + val result = projectAlong(v, normal, base) + + // assert that result is on plane + assertTrue(isOrthogonal(result - base, normal)) + // assert that PV vector is collinear to normal vector + assertTrue(isCollinear(v - result, normal)) + } + } + } + } + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionOntoLineTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionOntoLineTest.kt new file mode 100644 index 000000000..ab6ef3628 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/ProjectionOntoLineTest.kt @@ -0,0 +1,83 @@ +package space.kscience.kmath.geometry + +import kotlin.test.Test +import kotlin.test.assertTrue + +internal class ProjectionOntoLineTest { + @Test + fun projectionIntoOx() { + with(Euclidean2DSpace) { + val ox = Line(zero, Vector2D(1.0, 0.0)) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + assertVectorEquals(Vector2D(x, 0.0), projectToLine(Vector2D(x, y), ox)) + } + } + } + + @Test + fun projectionIntoOy() { + with(Euclidean2DSpace) { + val line = Line(zero, Vector2D(0.0, 1.0)) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + assertVectorEquals(Vector2D(0.0, y), projectToLine(Vector2D(x, y), line)) + } + } + } + + @Test + fun projectionIntoYEqualsX() { + with(Euclidean2DSpace) { + val line = Line(zero, Vector2D(1.0, 1.0)) + + assertVectorEquals(zero, projectToLine(zero, line)) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + val d = (y - x) / 2.0 + assertVectorEquals(Vector2D(x + d, y - d), projectToLine(Vector2D(x, y), line)) + } + } + } + + @Test + fun projectionOntoLine2d() { + with(Euclidean2DSpace) { + val a = 5.0 + val b = -3.0 + val c = -15.0 + val line = Line(Vector2D(3.0, 0.0), Vector2D(3.0, 5.0)) + + grid(-10.0..10.0, -10.0..10.0, 0.15).forEach { (x, y) -> + val xProj = (b * (b * x - a * y) - a * c) / (a * a + b * b) + val yProj = (a * (-b * x + a * y) - b * c) / (a * a + b * b) + assertVectorEquals(Vector2D(xProj, yProj), projectToLine(Vector2D(x, y), line)) + } + } + } + + @Test + fun projectionOntoLine3d() { + val line = Line3D( + base = Vector3D(1.0, 3.5, 0.07), + direction = Vector3D(2.0, -0.0037, 11.1111) + ) + + with(Euclidean3DSpace) { + val testDomain = (-10.0..10.0).generateList(0.43) + for (x in testDomain) { + for (y in testDomain) { + for (z in testDomain) { + val v = Vector3D(x, y, z) + val result = projectToLine(v, line) + + // assert that result is on line + assertTrue(isCollinear(result - line.base, line.direction)) + // assert that PV vector is orthogonal to direction vector + assertTrue(isOrthogonal(v - result, line.direction)) + } + } + } + } + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector2DTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector2DTest.kt new file mode 100644 index 000000000..84b1f4fd6 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector2DTest.kt @@ -0,0 +1,36 @@ +package space.kscience.kmath.geometry + +import space.kscience.kmath.structures.asSequence +import space.kscience.kmath.structures.toList +import kotlin.test.assertEquals +import kotlin.test.Test + +internal class Vector2DTest { + private val vector = Vector2D(1.0, -7.999) + + @Test + fun size() { + assertEquals(2, vector.size) + } + + @Test + fun get() { + assertEquals(1.0, vector[0]) + assertEquals(-7.999, vector[1]) + } + + @Test + fun iterator() { + assertEquals(listOf(1.0, -7.999), vector.toList()) + } + + @Test + fun x() { + assertEquals(1.0, vector.x) + } + + @Test + fun y() { + assertEquals(-7.999, vector.y) + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector3DTest.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector3DTest.kt new file mode 100644 index 000000000..717e78871 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/Vector3DTest.kt @@ -0,0 +1,41 @@ +package space.kscience.kmath.geometry + +import space.kscience.kmath.structures.toList +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class Vector3DTest { + private val vector = Vector3D(1.0, -7.999, 0.001) + + @Test + fun size() { + assertEquals(3, vector.size) + } + + @Test + fun get() { + assertEquals(1.0, vector[0]) + assertEquals(-7.999, vector[1]) + assertEquals(0.001, vector[2]) + } + + @Test + fun iterator() { + assertEquals(listOf(1.0, -7.999, 0.001), vector.toList()) + } + + @Test + fun x() { + assertEquals(1.0, vector.x) + } + + @Test + fun y() { + assertEquals(-7.999, vector.y) + } + + @Test + fun z() { + assertEquals(0.001, vector.z) + } +} diff --git a/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/testUtils.kt b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/testUtils.kt new file mode 100644 index 000000000..1277c0130 --- /dev/null +++ b/kmath-geometry/src/commonTest/kotlin/space/kscience/kmath/geometry/testUtils.kt @@ -0,0 +1,41 @@ +package space.kscience.kmath.geometry + +import kotlin.math.abs +import kotlin.test.assertEquals + +fun ClosedRange.generateList(step: Double): List = generateSequence(start) { previous -> + if (previous == Double.POSITIVE_INFINITY) return@generateSequence null + val next = previous + step + if (next > endInclusive) null else next +}.toList() + +fun grid( + xRange: ClosedRange, + yRange: ClosedRange, + step: Double +): List> { + val xs = xRange.generateList(step) + val ys = yRange.generateList(step) + + return xs.flatMap { x -> ys.map { y -> x to y } } +} + +fun assertVectorEquals(expected: Vector2D, actual: Vector2D, absoluteTolerance: Double = 1e-6) { + assertEquals(expected.x, actual.x, absoluteTolerance) + assertEquals(expected.y, actual.y, absoluteTolerance) +} + +fun assertVectorEquals(expected: Vector3D, actual: Vector3D, absoluteTolerance: Double = 1e-6) { + assertEquals(expected.x, actual.x, absoluteTolerance) + assertEquals(expected.y, actual.y, absoluteTolerance) + assertEquals(expected.z, actual.z, absoluteTolerance) +} + +fun GeometrySpace.isCollinear(a: V, b: V, absoluteTolerance: Double = 1e-6): Boolean { + val aDist = a.distanceTo(zero) + val bDist = b.distanceTo(zero) + return abs(aDist) < absoluteTolerance || abs(bDist) < absoluteTolerance || abs(abs((a dot b) / (aDist * bDist)) - 1) < absoluteTolerance +} + +fun GeometrySpace.isOrthogonal(a: V, b: V, absoluteTolerance: Double = 1e-6): Boolean = + abs(a dot b) < absoluteTolerance