Repository: lightly-ai/lightly

Issue:
ActiveLearningScores cannot be sent when of type numpy.float32
When the active learning scores are of type `numpy.float32`, they cannot be parsed to a swagger_type:
`AttributeError: 'numpy.float32' object has no attribute 'swagger_types'`
This is solved by either casting to `numpy.float64` or a plain python `float`.

- [x] Implement a check before sending the active learning scores, that `numpy.float32` are cast to `float` or` numpy.float64`


Discussion:

Resolving patch:
diff --git a/lightly/active_learning/config/sampler_config.py b/lightly/active_learning/config/sampler_config.py
--- a/lightly/active_learning/config/sampler_config.py
+++ b/lightly/active_learning/config/sampler_config.py
@@ -24,13 +24,12 @@ class SamplerConfig:
     Examples:
         >>> # sample 100 images with CORESET sampling
         >>> config = SamplerConfig(method=SamplingMethod.CORESET, n_samples=100)
-        >>> config = SamplerConfig(method='CORESET', n_samples=100)
         >>>
         >>> # give your sampling a name
-        >>> config = SamplerConfig(method='CORESET', n_samples=100, name='my-sampling')
+        >>> config = SamplerConfig(method=SamplingMethod.CORESET, n_samples=100, name='my-sampling')
         >>>
         >>> # use minimum distance between samples as stopping criterion
-        >>> config = SamplerConfig(method='CORESET', n_samples=-1, min_distance=0.1)
+        >>> config = SamplerConfig(method=SamplingMethod.CORESET, n_samples=-1, min_distance=0.1)
 
     """
     def __init__(self, method: SamplingMethod = SamplingMethod.CORESET, n_samples: int = 32, min_distance: float = -1,
diff --git a/lightly/api/api_workflow_sampling.py b/lightly/api/api_workflow_sampling.py
--- a/lightly/api/api_workflow_sampling.py
+++ b/lightly/api/api_workflow_sampling.py
@@ -55,6 +55,8 @@ def sampling(self, sampler_config: SamplerConfig, al_scores: Dict[str, List[np.n
             if preselected_tag_id is None:
                 raise ValueError
             for score_type, score_values in al_scores.items():
+                if isinstance(score_values, np.ndarray):
+                    score_values = score_values.astype(np.float64)
                 body = ActiveLearningScoreCreateRequest(score_type=score_type, scores=list(score_values))
                 self.scores_api.create_or_update_active_learning_score_by_tag_id(
                     body, dataset_id=self.dataset_id, tag_id=preselected_tag_id)


Test patch:
diff --git a/tests/active_learning/test_active_learning_agent.py b/tests/active_learning/test_active_learning_agent.py
--- a/tests/active_learning/test_active_learning_agent.py
+++ b/tests/active_learning/test_active_learning_agent.py
@@ -27,7 +27,7 @@ def test_agent(self):
                         sampler_config = SamplerConfig(n_samples=n_samples, method=method)
 
                     if sampler_config.method == SamplingMethod.CORESET:
-                        predictions = np.random.rand(len(agent.unlabeled_set), 10)
+                        predictions = np.random.rand(len(agent.unlabeled_set), 10).astype(np.float32)
                         predictions_normalized = predictions / np.sum(predictions, axis=1)[:, np.newaxis]
                         al_scorer = ScorerClassification(predictions_normalized)
                         chosen_filenames = agent.query(sampler_config=sampler_config, al_scorer=al_scorer)
diff --git a/tests/api/test_rest_parser.py b/tests/api/test_rest_parser.py
new file mode 100644
--- /dev/null
+++ b/tests/api/test_rest_parser.py
@@ -0,0 +1,32 @@
+import unittest
+
+import numpy as np
+
+from lightly.openapi_generated.swagger_client import ApiClient, ScoresApi, ActiveLearningScoreCreateRequest, \
+    SamplingMethod
+from lightly.openapi_generated.swagger_client.rest import ApiException
+
+
+class TestRestParser(unittest.TestCase):
+
+    @unittest.skip("This test only shows the error, it does not ensure it is solved.")
+    def test_parse_active_learning_scores(self):
+        score_value_tuple = (
+            np.random.normal(0, 1, size=(999,)).astype(np.float32),
+            np.random.normal(0, 1, size=(999,)).astype(np.float64),
+            [12.0] * 999
+        )
+        api_client = ApiClient()
+        self.scores_api = ScoresApi(api_client)
+        for i, score_values in enumerate(score_value_tuple):
+            with self.subTest(i=i, msg=str(type(score_values))):
+                body = ActiveLearningScoreCreateRequest(score_type=SamplingMethod.CORESET, scores=list(score_values))
+                if isinstance(score_values[0], float):
+                    with self.assertRaises(ApiException):
+                        self.scores_api.create_or_update_active_learning_score_by_tag_id(
+                            body, dataset_id="dataset_id_xyz", tag_id="tag_id_xyz")
+                else:
+                    with self.assertRaises(AttributeError):
+                        self.scores_api.create_or_update_active_learning_score_by_tag_id(
+                            body, dataset_id="dataset_id_xyz", tag_id="tag_id_xyz")
+
diff --git a/tests/api_workflow/mocked_api_workflow_client.py b/tests/api_workflow/mocked_api_workflow_client.py
--- a/tests/api_workflow/mocked_api_workflow_client.py
+++ b/tests/api_workflow/mocked_api_workflow_client.py
@@ -110,6 +110,8 @@ def get_tags_by_dataset_id(self, dataset_id, **kwargs):
 class MockedScoresApi(ScoresApi):
     def create_or_update_active_learning_score_by_tag_id(self, body, dataset_id, tag_id, **kwargs) -> \
             CreateEntityResponse:
+        if len(body.scores) > 0 and not isinstance(body.scores[0], float):
+            raise AttributeError
         response_ = CreateEntityResponse(id="sampled_tag_id_xyz")
         return response_