From b4531e216f2d8f4aa70f3574ba1279693a77edf2 Mon Sep 17 00:00:00 2001 From: Marcos Wagner Date: Mon, 30 Aug 2021 15:16:05 -0300 Subject: [PATCH 1/2] Adding Kmeans and Vector3Stats python interface and tests Signed-off-by: Marcos Wagner --- src/python/CMakeLists.txt | 2 + src/python/Kmeans.i | 66 ++++++++++++ src/python/Kmeans_TEST.py | 173 ++++++++++++++++++++++++++++++++ src/python/Vector3Stats.i | 51 ++++++++++ src/python/Vector3Stats_TEST.py | 142 ++++++++++++++++++++++++++ src/python/python.i | 2 + 6 files changed, 436 insertions(+) create mode 100644 src/python/Kmeans.i create mode 100644 src/python/Kmeans_TEST.py create mode 100644 src/python/Vector3Stats.i create mode 100644 src/python/Vector3Stats_TEST.py diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 997ef3942..bca88b283 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -70,6 +70,7 @@ if (PYTHONLIBS_FOUND) set(python_tests Angle_TEST GaussMarkovProcess_TEST + Kmeans_TEST Line2_TEST Line3_TEST python_TEST @@ -77,6 +78,7 @@ if (PYTHONLIBS_FOUND) SignalStats_TEST Vector2_TEST Vector3_TEST + Vector3Stats_TEST Vector4_TEST Temperature_TEST ) diff --git a/src/python/Kmeans.i b/src/python/Kmeans.i new file mode 100644 index 000000000..c1c26ac10 --- /dev/null +++ b/src/python/Kmeans.i @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * 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. + * +*/ + +%module kmeans +%{ +#include +#include +#include +%} + +%include "std_vector.i" +%template(vector_vector3d) std::vector>; +%template(vector_uint) std::vector; + +%inline %{ + struct ClusterOutput { + bool result; + std::vector> centroids; + std::vector labels; + }; +%} + +namespace ignition +{ + namespace math + { + class Kmeans + { + %rename("%(undercase)s", %$isfunction, %$ismember, %$not %$isconstructor) ""; + public: explicit Kmeans(const std::vector> &_obs); + public: virtual ~Kmeans(); + public: std::vector> Observations() const; + public: bool Observations(const std::vector> &_obs); + public: bool AppendObservations(const std::vector> &_obs); + + %pythoncode %{ + def cluster(self, _k): + cluster_output = self._cluster(_k) + return [cluster_output.result, + vector_vector3d(cluster_output.centroids), + vector_uint(cluster_output.labels)] + %} + }; + %extend Kmeans{ + inline ClusterOutput _cluster(int _k) { + ClusterOutput output; + output.result = (*$self).Cluster(_k, output.centroids, output.labels); + return output; + } + } + } +} diff --git a/src/python/Kmeans_TEST.py b/src/python/Kmeans_TEST.py new file mode 100644 index 000000000..493c837f3 --- /dev/null +++ b/src/python/Kmeans_TEST.py @@ -0,0 +1,173 @@ +# Copyright (C) 2021 Open Source Robotics Foundation +# +# 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. + +import unittest +from ignition.math import Kmeans +from ignition.math import Vector3d +from ignition.math import vector_vector3d + + +class TestKmeans(unittest.TestCase): + + def test_kmeans_constructor(self): + # Create some observations. + obs = list([]) + obs.append(Vector3d(1.0, 1.0, 0.0)) + obs.append(Vector3d(1.1, 1.0, 0.0)) + obs.append(Vector3d(1.2, 1.0, 0.0)) + obs.append(Vector3d(1.3, 1.0, 0.0)) + obs.append(Vector3d(1.4, 1.0, 0.0)) + obs.append(Vector3d(5.0, 1.0, 0.0)) + obs.append(Vector3d(5.1, 1.0, 0.0)) + obs.append(Vector3d(5.2, 1.0, 0.0)) + obs.append(Vector3d(5.3, 1.0, 0.0)) + obs.append(Vector3d(5.4, 1.0, 0.0)) + + # Initialize Kmeans with two partitions. + kmeans = Kmeans(obs) + + # ::GetObservations() + obs_copy = list(kmeans.observations()).copy() + for i in range(len(obs_copy)): + self.assertEqual(obs_copy[i], obs[i]) + + for idx, a in enumerate(obs_copy): + obs_copy[idx] = a + Vector3d(0.1, 0.2, 0.0) + + self.assertTrue(kmeans.observations(obs_copy)) + + obs_copy = list(kmeans.observations()).copy() + for i in range(len(obs_copy)): + self.assertEqual(obs_copy[i], obs[i] + Vector3d(0.1, 0.2, 0.0)) + self.assertTrue(kmeans.observations(obs)) + + # ::Cluster() + result, centroids, labels = kmeans.cluster(2) + self.assertTrue(result) + + # Check that there are two centroids. + self.assertEqual(len(centroids), 2) + + # Check that the observations are clustered properly. + self.assertEqual(labels[0], labels[1]) + self.assertEqual(labels[1], labels[2]) + self.assertEqual(labels[2], labels[3]) + self.assertEqual(labels[3], labels[4]) + + self.assertNotEqual(labels[4], labels[5]) + + self.assertEqual(labels[5], labels[6]) + self.assertEqual(labels[6], labels[7]) + self.assertEqual(labels[7], labels[8]) + self.assertEqual(labels[8], labels[9]) + + # Check that there are two centroids. + self.assertEqual(centroids.size(), 2) + # Check that the observations are clustered properly. + self.assertEqual(labels[0], labels[1]) + self.assertEqual(labels[1], labels[2]) + self.assertEqual(labels[2], labels[3]) + self.assertEqual(labels[3], labels[4]) + + self.assertNotEqual(labels[4], labels[5]) + + self.assertEqual(labels[5], labels[6]) + self.assertEqual(labels[6], labels[7]) + self.assertEqual(labels[7], labels[8]) + self.assertEqual(labels[8], labels[9]) + + # Check that there are two centroids. + self.assertEqual(centroids.size(), 2) + + # Check that the observations are clustered properly. + self.assertEqual(labels[0], labels[1]) + self.assertEqual(labels[1], labels[2]) + self.assertEqual(labels[2], labels[3]) + self.assertEqual(labels[3], labels[4]) + + self.assertNotEqual(labels[4], labels[5]) + + self.assertEqual(labels[5], labels[6]) + self.assertEqual(labels[6], labels[7]) + self.assertEqual(labels[7], labels[8]) + self.assertEqual(labels[8], labels[9]) + + # Check the centroids. + expected_centroid1 = Vector3d(1.2, 1.0, 0.0) + expected_centroid2 = Vector3d(5.2, 1.0, 0.0) + if (centroids[0] == expected_centroid1): + self.assertEqual(centroids[1], expected_centroid2) + elif (centroids[0] == expected_centroid2): + self.assertEqual(centroids[1], expected_centroid1) + else: + self.fail() + + # Try to use an empty observation vector. + obs_copy.clear() + self.assertFalse(kmeans.observations(obs_copy)) + + # Try to call 'Cluster()' with an empty vector. + kmeansEmpty = Kmeans(obs_copy) + result, centroids, labels = kmeansEmpty.cluster(2) + self.assertFalse(result) + + # Try to use a non positive k. + result, centroids, labels = kmeans.cluster(0) + self.assertFalse(result) + + # Try to use a k > num_observations. + result, centroids, labels = kmeans.cluster(len(obs) + 1) + self.assertFalse(result) + + def test_kmeans_append(self): + # Create some observations. + obs = list([]) + obs2 = list([]) + obs_total = list([]) + + obs.append(Vector3d(1.0, 1.0, 0.0)) + obs.append(Vector3d(1.1, 1.0, 0.0)) + obs.append(Vector3d(1.2, 1.0, 0.0)) + obs.append(Vector3d(1.3, 1.0, 0.0)) + obs.append(Vector3d(1.4, 1.0, 0.0)) + + obs2.append(Vector3d(5.0, 1.0, 0.0)) + obs2.append(Vector3d(5.1, 1.0, 0.0)) + obs2.append(Vector3d(5.2, 1.0, 0.0)) + obs2.append(Vector3d(5.3, 1.0, 0.0)) + obs2.append(Vector3d(5.4, 1.0, 0.0)) + + for elem in obs: + obs_total.append(elem) + + for elem in obs2: + obs_total.append(elem) + + # Initialize Kmeans with two partitions. + kmeans = Kmeans(obs) + + kmeans.append_observations(obs2) + + obs_copy = vector_vector3d(kmeans.observations()) + + for i in range(obs_copy.size()): + self.assertEqual(obs_total[i], obs_copy[i]) + + # Append an empty vector. + emptyVector = vector_vector3d() + self.assertFalse(kmeans.append_observations(emptyVector)) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/python/Vector3Stats.i b/src/python/Vector3Stats.i new file mode 100644 index 000000000..460733282 --- /dev/null +++ b/src/python/Vector3Stats.i @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 Open Source Robotics Foundation + * + * 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. + * +*/ + +%module vector3stats +%{ +#include +#include +#include +#include +%} + +%include "std_string.i" + +namespace ignition +{ + namespace math + { + class Vector3Stats + { + %rename("%(undercase)s", %$isfunction, %$ismember, %$not %$isconstructor) ""; + public: Vector3Stats(); + public: ~Vector3Stats(); + public: void InsertData(const Vector3 &_data); + public: bool InsertStatistic(const std::string &_name); + public: bool InsertStatistics(const std::string &_names); + public: void Reset(); + public: const SignalStats &X() const; + public: const SignalStats &Y() const; + public: const SignalStats &Z() const; + public: const SignalStats &Mag() const; + public: SignalStats &X(); + public: SignalStats &Y(); + public: SignalStats &Z(); + public: SignalStats &Mag(); + }; + } +} diff --git a/src/python/Vector3Stats_TEST.py b/src/python/Vector3Stats_TEST.py new file mode 100644 index 000000000..293c8bda6 --- /dev/null +++ b/src/python/Vector3Stats_TEST.py @@ -0,0 +1,142 @@ +# Copyright (C) 2021 Open Source Robotics Foundation +# +# 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. + +import unittest +from ignition.math import Vector3d +from ignition.math import Vector3Stats + + +class TestVector3Stats(unittest.TestCase): + + stats = Vector3Stats() + + def x(self, _name): + return self.stats.x().map()[_name] + + def y(self, _name): + return self.stats.y().map()[_name] + + def z(self, _name): + return self.stats.z().map()[_name] + + def mag(self, _name): + return self.stats.mag().map()[_name] + + def test_vector3stats_constructor(self): + # Constructor + v3stats = Vector3Stats() + self.assertTrue(v3stats.x().map().empty()) + self.assertTrue(v3stats.y().map().empty()) + self.assertTrue(v3stats.z().map().empty()) + self.assertTrue(v3stats.mag().map().empty()) + self.assertAlmostEqual(v3stats.x().count(), 0) + self.assertAlmostEqual(v3stats.y().count(), 0) + self.assertAlmostEqual(v3stats.z().count(), 0) + self.assertAlmostEqual(v3stats.mag().count(), 0) + + # Reset + v3stats.reset() + self.assertTrue(v3stats.x().map().empty()) + self.assertTrue(v3stats.y().map().empty()) + self.assertTrue(v3stats.z().map().empty()) + self.assertTrue(v3stats.mag().map().empty()) + self.assertAlmostEqual(v3stats.x().count(), 0) + self.assertAlmostEqual(v3stats.y().count(), 0) + self.assertAlmostEqual(v3stats.z().count(), 0) + self.assertAlmostEqual(v3stats.mag().count(), 0) + + # InsertStatistics + v3stats = Vector3Stats() + self.assertTrue(v3stats.x().map().empty()) + self.assertTrue(v3stats.y().map().empty()) + self.assertTrue(v3stats.z().map().empty()) + self.assertTrue(v3stats.mag().map().empty()) + + self.assertTrue(v3stats.insert_statistics("maxAbs")) + self.assertFalse(v3stats.insert_statistics("maxAbs")) + self.assertFalse(v3stats.insert_statistic("maxAbs")) + self.assertFalse(v3stats.x().map().empty()) + self.assertFalse(v3stats.y().map().empty()) + self.assertFalse(v3stats.z().map().empty()) + self.assertFalse(v3stats.mag().map().empty()) + + # Map with no data + map = v3stats.x().map() + self.assertAlmostEqual(map.size(), 1) + self.assertAlmostEqual(map.count("maxAbs"), 1) + + map = v3stats.y().map() + self.assertAlmostEqual(map.size(), 1) + self.assertAlmostEqual(map.count("maxAbs"), 1) + + map = v3stats.z().map() + self.assertAlmostEqual(map.size(), 1) + self.assertAlmostEqual(map.count("maxAbs"), 1) + + map = v3stats.mag().map() + self.assertAlmostEqual(map.size(), 1) + self.assertAlmostEqual(map.count("maxAbs"), 1) + + # Insert some data + self.assertAlmostEqual(v3stats.x().count(), 0) + self.assertAlmostEqual(v3stats.y().count(), 0) + self.assertAlmostEqual(v3stats.z().count(), 0) + self.assertAlmostEqual(v3stats.mag().count(), 0) + + v3stats.insert_data(Vector3d.UNIT_X) + v3stats.insert_data(Vector3d.UNIT_X) + v3stats.insert_data(Vector3d.UNIT_Y) + + self.assertAlmostEqual(v3stats.x().count(), 3) + self.assertAlmostEqual(v3stats.y().count(), 3) + self.assertAlmostEqual(v3stats.z().count(), 3) + self.assertAlmostEqual(v3stats.mag().count(), 3) + + self.assertAlmostEqual(v3stats.x().map()["maxAbs"], 1.0, delta=1e-10) + self.assertAlmostEqual(v3stats.y().map()["maxAbs"], 1.0, delta=1e-10) + self.assertAlmostEqual(v3stats.z().map()["maxAbs"], 0.0) + self.assertAlmostEqual(v3stats.mag().map()["maxAbs"], 1.0, delta=1e-10) + + def test_vector3stats_const_accessor(self): + # Const accessors + self.assertTrue(self.stats.x().map().empty()) + self.assertTrue(self.stats.y().map().empty()) + self.assertTrue(self.stats.z().map().empty()) + self.assertTrue(self.stats.mag().map().empty()) + + name = "maxAbs" + self.assertTrue(self.stats.insert_statistics(name)) + + self.stats.insert_data(Vector3d.UNIT_X) + self.stats.insert_data(Vector3d.UNIT_X) + self.stats.insert_data(Vector3d.UNIT_Y) + + self.assertAlmostEqual(self.stats.x().count(), 3) + self.assertAlmostEqual(self.stats.y().count(), 3) + self.assertAlmostEqual(self.stats.z().count(), 3) + self.assertAlmostEqual(self.stats.mag().count(), 3) + + self.assertAlmostEqual(self.stats.x().map()[name], 1.0, delta=1e-10) + self.assertAlmostEqual(self.stats.y().map()[name], 1.0, delta=1e-10) + self.assertAlmostEqual(self.stats.z().map()[name], 0.0) + self.assertAlmostEqual(self.stats.mag().map()[name], 1.0, delta=1e-10) + + self.assertAlmostEqual(self.x(name), 1.0, delta=1e-10) + self.assertAlmostEqual(self.y(name), 1.0, delta=1e-10) + self.assertAlmostEqual(self.z(name), 0.0) + self.assertAlmostEqual(self.mag(name), 1.0, delta=1e-10) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/python/python.i b/src/python/python.i index 4aab42ac7..2f8e6d64c 100644 --- a/src/python/python.i +++ b/src/python/python.i @@ -9,3 +9,5 @@ %include Line3.i %include SignalStats.i %include Temperature.i +%include Kmeans.i +%include Vector3Stats.i From c793d317fd4b51431ead76108b30743593eaf671 Mon Sep 17 00:00:00 2001 From: Steve Peters Date: Wed, 8 Sep 2021 07:01:40 -0700 Subject: [PATCH 2/2] Kmeans_TEST.py: remove duplicate code Signed-off-by: Steve Peters --- src/python/Kmeans_TEST.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/python/Kmeans_TEST.py b/src/python/Kmeans_TEST.py index 493c837f3..62811835b 100644 --- a/src/python/Kmeans_TEST.py +++ b/src/python/Kmeans_TEST.py @@ -72,37 +72,6 @@ def test_kmeans_constructor(self): self.assertEqual(labels[7], labels[8]) self.assertEqual(labels[8], labels[9]) - # Check that there are two centroids. - self.assertEqual(centroids.size(), 2) - # Check that the observations are clustered properly. - self.assertEqual(labels[0], labels[1]) - self.assertEqual(labels[1], labels[2]) - self.assertEqual(labels[2], labels[3]) - self.assertEqual(labels[3], labels[4]) - - self.assertNotEqual(labels[4], labels[5]) - - self.assertEqual(labels[5], labels[6]) - self.assertEqual(labels[6], labels[7]) - self.assertEqual(labels[7], labels[8]) - self.assertEqual(labels[8], labels[9]) - - # Check that there are two centroids. - self.assertEqual(centroids.size(), 2) - - # Check that the observations are clustered properly. - self.assertEqual(labels[0], labels[1]) - self.assertEqual(labels[1], labels[2]) - self.assertEqual(labels[2], labels[3]) - self.assertEqual(labels[3], labels[4]) - - self.assertNotEqual(labels[4], labels[5]) - - self.assertEqual(labels[5], labels[6]) - self.assertEqual(labels[6], labels[7]) - self.assertEqual(labels[7], labels[8]) - self.assertEqual(labels[8], labels[9]) - # Check the centroids. expected_centroid1 = Vector3d(1.2, 1.0, 0.0) expected_centroid2 = Vector3d(5.2, 1.0, 0.0)