smqtk_classifier 0.20.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. smqtk_classifier/__init__.py +9 -0
  2. smqtk_classifier/_defaults.py +8 -0
  3. smqtk_classifier/classification_element_factory.py +116 -0
  4. smqtk_classifier/classify_descriptor_collection.py +298 -0
  5. smqtk_classifier/exceptions.py +29 -0
  6. smqtk_classifier/impls/__init__.py +0 -0
  7. smqtk_classifier/impls/classification_element/__init__.py +0 -0
  8. smqtk_classifier/impls/classification_element/file.py +125 -0
  9. smqtk_classifier/impls/classification_element/memory.py +77 -0
  10. smqtk_classifier/impls/classification_element/postgres.py +263 -0
  11. smqtk_classifier/impls/classify_descriptor/__init__.py +0 -0
  12. smqtk_classifier/impls/classify_descriptor/classify_index_label_descriptor.py +63 -0
  13. smqtk_classifier/impls/classify_descriptor_supervised/__init__.py +0 -0
  14. smqtk_classifier/impls/classify_descriptor_supervised/libsvm.py +420 -0
  15. smqtk_classifier/impls/classify_descriptor_supervised/sklearn_logistic_regression.py +84 -0
  16. smqtk_classifier/impls/classify_descriptor_supervised/sklearn_svm.py +232 -0
  17. smqtk_classifier/interfaces/__init__.py +0 -0
  18. smqtk_classifier/interfaces/classification_element.py +264 -0
  19. smqtk_classifier/interfaces/classify_descriptor.py +399 -0
  20. smqtk_classifier/interfaces/classify_descriptor_supervised.py +75 -0
  21. smqtk_classifier/interfaces/classify_image.py +60 -0
  22. smqtk_classifier/interfaces/classify_image_supervised.py +73 -0
  23. smqtk_classifier/py.typed +0 -0
  24. smqtk_classifier-0.20.0.dist-info/LICENSE.txt +27 -0
  25. smqtk_classifier-0.20.0.dist-info/METADATA +55 -0
  26. smqtk_classifier-0.20.0.dist-info/RECORD +28 -0
  27. smqtk_classifier-0.20.0.dist-info/WHEEL +4 -0
  28. smqtk_classifier-0.20.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,9 @@
1
+ from .interfaces.classification_element import ClassificationElement # noqa: F401
2
+ from .interfaces.classify_descriptor import ClassifyDescriptor # noqa: F401
3
+ from .interfaces.classify_descriptor_supervised import ClassifyDescriptorSupervised # noqa: F401
4
+ from .interfaces.classify_image import ClassifyImage # noqa: F401
5
+ from .interfaces.classify_image_supervised import ClassifyImageSupervised # noqa: F401
6
+
7
+ from .classification_element_factory import ClassificationElementFactory # noqa: F401
8
+ from .classify_descriptor_collection import ClassifyDescriptorCollection # noqa: F401
9
+ from .classify_descriptor_collection import ClassifierCollection # noqa: F401
@@ -0,0 +1,8 @@
1
+ from smqtk_classifier.classification_element_factory import ClassificationElementFactory
2
+ from smqtk_classifier.impls.classification_element.memory import MemoryClassificationElement
3
+
4
+
5
+ # Default classifier element factory for interfaces.
6
+ DFLT_CLASSIFIER_FACTORY = ClassificationElementFactory(
7
+ MemoryClassificationElement, {}
8
+ )
@@ -0,0 +1,116 @@
1
+ from typing import Any, Dict, Hashable, Type, TypeVar
2
+
3
+ from smqtk_core import Configurable
4
+ from smqtk_core.configuration import (
5
+ cls_conf_from_config_dict,
6
+ cls_conf_to_config_dict,
7
+ make_default_config
8
+ )
9
+ from smqtk_core.dict import merge_dict
10
+
11
+ from smqtk_classifier.interfaces.classification_element import ClassificationElement
12
+
13
+
14
+ C = TypeVar("C", bound=ClassificationElement)
15
+
16
+
17
+ class ClassificationElementFactory (Configurable):
18
+ """
19
+ Factory class for producing ClassificationElement instances of a specified
20
+ type and configuration.
21
+ """
22
+
23
+ # noinspection PyShadowingBuiltins
24
+ def __init__(self, type: Type[C], type_config: Dict[str, Any]):
25
+ """
26
+ Initialize the factory to produce ClassificationElement instances of the
27
+ given type from the given configuration.
28
+
29
+ :param type: Python implementation type of the ClassifierElement to
30
+ produce
31
+ :param type_config: Configuration dictionary that will be passed
32
+ ``from_config`` class method of given ``type``.
33
+ """
34
+ self.type = type
35
+ self.type_config = type_config
36
+
37
+ #
38
+ # Class methods
39
+ #
40
+
41
+ @classmethod
42
+ def get_default_config(cls) -> Dict[str, Any]:
43
+ """
44
+ Generate and return a default configuration dictionary for this class.
45
+ This will be primarily used for generating what the configuration
46
+ dictionary would look like for this class without instantiating it.
47
+
48
+ It is not be guaranteed that the configuration dictionary returned
49
+ from this method is valid for construction of an instance of this class.
50
+
51
+ :return: Default configuration dictionary for the class.
52
+ :rtype: dict
53
+
54
+ """
55
+ return make_default_config(ClassificationElement.get_impls())
56
+
57
+ # We likely do not expect subclasses for this type base, thus it is OK to
58
+ # use direct type reference in Type and return annotations.
59
+ @classmethod
60
+ def from_config(
61
+ cls: Type["ClassificationElementFactory"],
62
+ config_dict: Dict,
63
+ merge_default: bool = True
64
+ ) -> "ClassificationElementFactory":
65
+ """
66
+ Instantiate a new instance of this class given the configuration
67
+ JSON-compliant dictionary encapsulating initialization arguments.
68
+
69
+ This method should not be called via super unless and instance of the
70
+ class is desired.
71
+
72
+ :param config_dict: JSON compliant dictionary encapsulating
73
+ a configuration.
74
+ :param merge_default: Merge the given configuration on top of the
75
+ default provided by ``get_default_config``.
76
+
77
+ :return: Constructed instance from the provided config.
78
+ """
79
+ if merge_default:
80
+ config_dict = merge_dict(cls.get_default_config(), config_dict)
81
+
82
+ ce_type, ce_conf = cls_conf_from_config_dict(
83
+ config_dict, ClassificationElement.get_impls()
84
+ )
85
+ return ClassificationElementFactory(ce_type, ce_conf)
86
+
87
+ def get_config(self) -> Dict[str, Any]:
88
+ return cls_conf_to_config_dict(self.type, self.type_config)
89
+
90
+ # noinspection PyShadowingBuiltins
91
+ def new_classification(self, type: str, uuid: Hashable) -> ClassificationElement:
92
+ """
93
+ Create a new ClassificationElement instance of the configured
94
+ implementation.
95
+
96
+ :param type: Type of classifier. This is usually the name of the
97
+ classifier that generated this result.
98
+ :param uuid: UUID to associate with the classification.
99
+
100
+ :return: New ClassificationElement instance.
101
+ """
102
+ return self.type.from_config(self.type_config, type, uuid)
103
+
104
+ # noinspection PyShadowingBuiltins
105
+ def __call__(self, type: str, uuid: Hashable) -> ClassificationElement:
106
+ """
107
+ Create a new ClassificationElement instance of the configured
108
+ implementation.
109
+
110
+ :param type: Type of classifier. This is usually the name of the
111
+ classifier that generated this result.
112
+ :param uuid: UUID to associate with the classification.
113
+
114
+ :return: New ClassificationElement instance.
115
+ """
116
+ return self.new_classification(type, uuid)
@@ -0,0 +1,298 @@
1
+ import threading
2
+ from typing import Any, Dict, Hashable, Iterable, List, Optional, Mapping, Sequence, Set, Type
3
+ from types import TracebackType
4
+ from warnings import warn
5
+ import numpy as np
6
+
7
+ from smqtk_core.configuration import (
8
+ Configurable,
9
+ make_default_config,
10
+ from_config_dict,
11
+ to_config_dict
12
+ )
13
+ from smqtk_core.dict import merge_dict
14
+ from smqtk_descriptors import DescriptorElement
15
+
16
+ from smqtk_classifier.classification_element_factory import ClassificationElementFactory
17
+ from smqtk_classifier.exceptions import MissingLabelError
18
+ from smqtk_classifier.interfaces.classification_element import ClassificationElement
19
+
20
+ from ._defaults import DFLT_CLASSIFIER_FACTORY
21
+ from .interfaces.classify_descriptor import ClassifyDescriptor
22
+
23
+
24
+ class ClassifyDescriptorCollection (Configurable):
25
+ """
26
+ A collection of descriptively-labeled classifier instances for the purpose
27
+ of applying all stored classifiers to one or more input descriptor
28
+ elements.
29
+
30
+ TODO: [optionally?] map a classification element factory per classifier.
31
+
32
+ :param classifiers: Optional dictionary of semantic label keys and
33
+ Classifier instance values.
34
+ :param labeled_classifiers: Key-word arguments may be provided where
35
+ the key used is considered the semantic label of the provided
36
+ Classifier instance.
37
+ """
38
+
39
+ EXAMPLE_KEY = '__example_label__'
40
+
41
+ def __init__(
42
+ self,
43
+ classifiers: Mapping[str, ClassifyDescriptor] | None = None,
44
+ **labeled_classifiers: ClassifyDescriptor
45
+ ):
46
+ self._label_to_classifier_lock = threading.RLock()
47
+ self._label_to_classifier = {}
48
+
49
+ # Go though classifiers map and key-word arguments, check that values
50
+ # are actually classifiers.
51
+ if classifiers is not None:
52
+ for label, classifier in classifiers.items():
53
+ if not isinstance(classifier, ClassifyDescriptor):
54
+ raise ValueError("Found a non-Classifier instance value "
55
+ "for key '%s'" % label)
56
+ self._label_to_classifier[label] = classifier
57
+
58
+ for label, classifier in labeled_classifiers.items():
59
+ if not isinstance(classifier, ClassifyDescriptor):
60
+ raise ValueError("Found a non-Classifier instance value "
61
+ "for key '%s'" % label)
62
+ elif label in self._label_to_classifier:
63
+ raise ValueError("Duplicate classifier label '%s' provided "
64
+ "in key-word arguments." % label)
65
+ self._label_to_classifier[label] = classifier
66
+
67
+ @classmethod
68
+ def get_default_config(cls) -> Dict[str, Any]:
69
+ c = super(ClassifyDescriptorCollection, cls).get_default_config()
70
+
71
+ # We list the label-classifier mapping on one level, so remove the
72
+ # nested map parameter that can optionally be used in the constructor.
73
+ del c['classifiers']
74
+
75
+ # Add slot of a list of classifier plugin specifications
76
+ c[cls.EXAMPLE_KEY] = make_default_config(ClassifyDescriptor.get_impls())
77
+
78
+ return c
79
+
80
+ # We likely do not expect subclasses for this type base, thus it is OK to
81
+ # use direct type reference in Type and return annotations.
82
+ @classmethod
83
+ def from_config(
84
+ cls,
85
+ config_dict: Dict,
86
+ merge_default: bool = True
87
+ ) -> "ClassifyDescriptorCollection":
88
+ if merge_default:
89
+ config_dict = merge_dict(cls.get_default_config(), config_dict)
90
+
91
+ classifier_map = {}
92
+
93
+ # Copying list of keys so we can update the dictionary as we loop.
94
+ for label in list(config_dict.keys()):
95
+ # Skip the example section.
96
+ if label == cls.EXAMPLE_KEY:
97
+ continue
98
+
99
+ classifier_config = config_dict[label]
100
+ classifier = from_config_dict(classifier_config,
101
+ ClassifyDescriptor.get_impls())
102
+ classifier_map[label] = classifier
103
+
104
+ return cls(classifiers=classifier_map)
105
+
106
+ def get_config(self) -> Dict[str, Any]:
107
+ with self._label_to_classifier_lock:
108
+ c = dict((label, to_config_dict(classifier))
109
+ for label, classifier
110
+ in self._label_to_classifier.items())
111
+ return c
112
+
113
+ def __enter__(self) -> "ClassifyDescriptorCollection":
114
+ """
115
+ :rtype: IqrSession
116
+ """
117
+ self._label_to_classifier_lock.acquire()
118
+ return self
119
+
120
+ def __exit__(
121
+ self,
122
+ exc_type: Optional[Type],
123
+ exc_val: Optional[BaseException],
124
+ exc_tb: Optional[TracebackType]
125
+ ) -> None:
126
+ self._label_to_classifier_lock.release()
127
+
128
+ def size(self) -> int:
129
+ with self._label_to_classifier_lock:
130
+ return len(self._label_to_classifier)
131
+
132
+ __len__ = size
133
+
134
+ def labels(self) -> Set[str]:
135
+ """
136
+ :return: Set of labels for currently collected classifiers.
137
+ """
138
+ with self._label_to_classifier_lock:
139
+ return set(self._label_to_classifier.keys())
140
+
141
+ def add_classifier(self, label: str, classifier: ClassifyDescriptor) -> "ClassifyDescriptorCollection":
142
+ """
143
+ Add a classifier instance with associated descriptive label to this
144
+ collection.
145
+
146
+ :param label: String descriptive label for the classifier.
147
+ :param classifier: Classifier instance to collect.
148
+
149
+ :raises ValueError: Classifier provided is not actually a classifier
150
+ instance, or if the label provided already exists in this
151
+ collection.
152
+
153
+ :return: Self.
154
+ """
155
+ if not isinstance(classifier, ClassifyDescriptor):
156
+ raise ValueError("Not given a Classifier instance (given type"
157
+ " %s)." % type(classifier))
158
+ with self._label_to_classifier_lock:
159
+ if label in self._label_to_classifier:
160
+ raise ValueError("Duplicate label provided: '%s'" % label)
161
+ self._label_to_classifier[label] = classifier
162
+ return self
163
+
164
+ def get_classifier(self, label: str) -> ClassifyDescriptor:
165
+ """
166
+ Get the classifier instance for a given label.
167
+
168
+ :param label: Label of the classifier to get.
169
+
170
+ :raises KeyError: No classifier for the given label.
171
+
172
+ :return: Classifier instance.
173
+ """
174
+ with self._label_to_classifier_lock:
175
+ return self._label_to_classifier[label]
176
+
177
+ def remove_classifier(self, label: str) -> "ClassifyDescriptorCollection":
178
+ """
179
+ Remove a label-classifier pair from this collection.
180
+
181
+ :param label: Label of the classifier to remove.
182
+
183
+ :raises KeyError: The given label does not reference a classifier in
184
+ this collection.
185
+
186
+ :return: Self.
187
+ """
188
+ with self._label_to_classifier_lock:
189
+ del self._label_to_classifier[label]
190
+ return self
191
+
192
+ def labels_to_classifiers(self, labels: Optional[Iterable[str]] = None) -> Dict[str, ClassifyDescriptor]:
193
+ """
194
+ Get a shallow copy mapping of classifiers for the labels given, or for
195
+ all classifiers if no labels were explicitly given.
196
+
197
+ This method is thread-safe and the returned dictionary is separate from
198
+ this class's control. However, the classifier instances are still
199
+ shared.
200
+
201
+ :param None | typing.Iterable[str] labels:
202
+ One or more labels of stored classifiers to retrieve.
203
+ If None, we will consider all stored classifiers.
204
+
205
+ :raises MissingLabelError: Thrown when one or more labels provided do
206
+ not associate to any currently stored classifiers.
207
+
208
+ :return: Dictionary mapping string labels to Classifier instances.
209
+ """
210
+ with self._label_to_classifier_lock:
211
+ if labels is not None:
212
+ labels = list(labels)
213
+ # If we're missing some of the requested labels, complain
214
+ missing_labels = set(labels) - self.labels()
215
+ if missing_labels:
216
+ raise MissingLabelError(missing_labels)
217
+ label2classifier = {label: self._label_to_classifier[label]
218
+ for label in labels}
219
+ else:
220
+ label2classifier = dict(self._label_to_classifier)
221
+ return label2classifier
222
+
223
+ def classify(
224
+ self,
225
+ descriptor: DescriptorElement,
226
+ labels: Optional[Iterable[str]] = None,
227
+ factory: ClassificationElementFactory = DFLT_CLASSIFIER_FACTORY,
228
+ overwrite: bool = False
229
+ ) -> Dict[str, ClassificationElement]:
230
+ """
231
+ Apply all stored classifiers to the given descriptor element.
232
+
233
+ We return a dictionary mapping the label of a stored classifier to the
234
+ classifier element result produced by that classifier via the
235
+ provided classification element factory.
236
+
237
+ :param descriptor: Descriptor element to classify.
238
+ :param labels: One or more labels of stored classifiers to use for
239
+ classifying the given descriptor. If None, use all stored
240
+ classifiers.
241
+ :param factory: Classification element factory.
242
+ :param overwrite: Force re-computation of the classification of the
243
+ input descriptor.
244
+
245
+ :raises smqtk.exceptions.MissingLabelError: Some or all of the
246
+ requested labels are missing.
247
+
248
+ :return: Result dictionary of classifier labels to classification
249
+ elements.
250
+ """
251
+ d_classifications = {}
252
+ label2classifier = self.labels_to_classifiers(labels)
253
+ for label, classifier in label2classifier.items():
254
+ d_classifications[label] = classifier.classify_one_element(
255
+ descriptor, factory=factory, overwrite=overwrite
256
+ )
257
+ return d_classifications
258
+
259
+ def classify_arrays(
260
+ self,
261
+ array_seq: Sequence[np.ndarray],
262
+ labels: Sequence[str] | None = None
263
+ ) -> Dict[str, List[Dict[Hashable, float]]]:
264
+ """
265
+ Apply all stored classifiers to the given iterable or matrix of numpy
266
+ arrays.
267
+
268
+ We return a dictionary mapping the label of a stored classifier to the
269
+ class-confidence map result produced by that classifier's
270
+ `classify_arrays` method.
271
+
272
+ :param Sequence[numpy.ndarray] array_seq:
273
+ Sequence of descriptor vectors, as numpy arrays, to be classified.
274
+ :param Sequence[str] labels:
275
+ One or more labels of stored classifiers to use for classifying the
276
+ given descriptors. If None (the default), use all stored
277
+ classifiers.
278
+
279
+ :raises MissingLabelError: Thrown when one or more labels provided do
280
+ not associate to any currently stored classifiers.
281
+
282
+ :return: Dictionary of result predictions for each classifier.
283
+ :rtype: dict[str, list[dict[collections.abc.Hashable, float]]]
284
+ """
285
+ label2classifier = self.labels_to_classifiers(labels)
286
+ label2pred = {}
287
+ for label, classifier in label2classifier.items():
288
+ label2pred[label] = list(classifier.classify_arrays(array_seq))
289
+ return label2pred
290
+
291
+
292
+ class ClassifierCollection(ClassifyDescriptorCollection):
293
+
294
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
295
+ warn("ClassifierCollection was renamed to "
296
+ "ClassifyDescriptorCollection", category=DeprecationWarning,
297
+ stacklevel=2)
298
+ super().__init__(*args, **kwargs)
@@ -0,0 +1,29 @@
1
+ from typing import Set
2
+
3
+
4
+ class NoClassificationError (Exception):
5
+ """
6
+ When a ClassificationElement has no mapping yet set, but an operation
7
+ required it.
8
+ """
9
+
10
+
11
+ class MissingLabelError(Exception):
12
+ """
13
+ Raised by ClassifyDescriptorCollection.classify when requested classifier
14
+ labels are missing from collection.
15
+ """
16
+ def __init__(self, labels: Set[str]):
17
+ """
18
+ :param labels: The labels missing from the collection
19
+ """
20
+ super(MissingLabelError, self).__init__(labels)
21
+ self.labels = labels
22
+
23
+
24
+ class ExistingModelError(Exception):
25
+ """
26
+ Raised by ClassifyDescriptorSupervised and ClassifyImageSupervised when
27
+ a model already exists in an instance to prevent overwriting and existing
28
+ model
29
+ """
File without changes
@@ -0,0 +1,125 @@
1
+ import pickle
2
+ import os.path as osp
3
+ from typing import Any, Dict, Hashable, Optional
4
+
5
+ from smqtk_dataprovider.utils.file import safe_create_dir
6
+ from smqtk_dataprovider.utils.string import partition_string
7
+
8
+ from smqtk_classifier.exceptions import NoClassificationError
9
+ from smqtk_classifier.interfaces.classification_element import (
10
+ ClassificationElement,
11
+ CLASSIFICATION_DICT_T,
12
+ CLASSIFICATION_MAP_T
13
+ )
14
+
15
+
16
+ class FileClassificationElement (ClassificationElement): # lgtm [py/missing-equals]
17
+ """
18
+ Create a classification element from a cached file representation.
19
+
20
+ :param type_name: Type of classification. This is usually the name of
21
+ the classifier that generated this result.
22
+ :type type_name: str
23
+
24
+ :param uuid: uuid for this classification
25
+ :type uuid: collections.abc.Hashable
26
+
27
+ :param save_dir: Directory to save this element's contents. If this path
28
+ is relative, we interpret as relative to the current working
29
+ directory.
30
+ :type save_dir: str | unicode
31
+
32
+ :param subdir_split: If a positive integer, this will cause us to store
33
+ the vector file in a subdirectory under the ``save_dir`` that was
34
+ specified. The integer value specifies the number of splits that we
35
+ will make in the stringification of this descriptor's UUID. If
36
+ there happen to be dashes in this stringification, we will
37
+ remove them (as would happen if given an uuid.UUID instance as
38
+ the uuid element).
39
+ :type subdir_split: None | int
40
+
41
+ :param pickle_protocol: Pickling protocol to use. We will use -1 by
42
+ default (latest version, probably binary).
43
+ :type pickle_protocol: int
44
+ """
45
+
46
+ __slots__ = ('save_dir', 'pickle_protocol', 'subdir_split', 'filepath')
47
+
48
+ def __init__(
49
+ self,
50
+ type_name: str,
51
+ uuid: Hashable,
52
+ save_dir: str,
53
+ subdir_split: Optional[int] = None,
54
+ pickle_protocol: int = -1
55
+ ):
56
+ super(FileClassificationElement, self).__init__(type_name, uuid)
57
+
58
+ # TODO: Remove absolute path conversion (allow relative)
59
+ self.save_dir = osp.abspath(osp.expanduser(save_dir))
60
+ self.pickle_protocol = pickle_protocol
61
+
62
+ # Saving components
63
+ self.subdir_split = subdir_split
64
+ if subdir_split and int(subdir_split) > 0:
65
+ self.subdir_split = subdir_split = int(subdir_split)
66
+ # Using all but the last split segment. This is so we don't create
67
+ # a whole bunch of directories with a single element in them.
68
+ save_dir = osp.join(self.save_dir,
69
+ *partition_string(str(uuid).replace('-', ''),
70
+ subdir_split)[:subdir_split-1]
71
+ )
72
+ else:
73
+ save_dir = self.save_dir
74
+
75
+ self.filepath = osp.join(save_dir,
76
+ "%s.%s.classification.pickle"
77
+ % (self.type_name, str(self.uuid)))
78
+
79
+ @classmethod
80
+ def is_usable(cls) -> bool:
81
+ return True
82
+
83
+ def get_config(self) -> Dict[str, Any]:
84
+ return {
85
+ "save_dir": self.save_dir,
86
+ 'subdir_split': self.subdir_split,
87
+ "pickle_protocol": self.pickle_protocol,
88
+ }
89
+
90
+ def __getstate__(self) -> Any:
91
+ return (
92
+ super(FileClassificationElement, self).__getstate__(),
93
+ self.save_dir,
94
+ self.pickle_protocol,
95
+ self.subdir_split,
96
+ self.filepath,
97
+ )
98
+
99
+ def __setstate__(self, state: Any) -> None:
100
+ super(FileClassificationElement, self).__setstate__(state[0])
101
+ self.save_dir, \
102
+ self.pickle_protocol, \
103
+ self.subdir_split, \
104
+ self.filepath \
105
+ = state[1:]
106
+
107
+ def has_classifications(self) -> bool:
108
+ return osp.isfile(self.filepath)
109
+
110
+ def get_classification(self) -> CLASSIFICATION_DICT_T:
111
+ if not self.has_classifications():
112
+ raise NoClassificationError("No classification values.")
113
+ with open(self.filepath, 'rb') as f:
114
+ return pickle.load(f)
115
+
116
+ def set_classification(
117
+ self,
118
+ m: Optional[CLASSIFICATION_MAP_T] = None,
119
+ **kwds: float
120
+ ) -> CLASSIFICATION_DICT_T:
121
+ m = super(FileClassificationElement, self).set_classification(m, **kwds)
122
+ safe_create_dir(osp.dirname(self.filepath))
123
+ with open(self.filepath, 'wb') as f:
124
+ pickle.dump(m, f, self.pickle_protocol)
125
+ return m
@@ -0,0 +1,77 @@
1
+ from threading import RLock
2
+ from typing import Any, Dict, Hashable, Optional
3
+
4
+ from smqtk_classifier.exceptions import NoClassificationError
5
+ from smqtk_classifier.interfaces.classification_element import (
6
+ ClassificationElement, CLASSIFICATION_MAP_T, CLASSIFICATION_DICT_T
7
+ )
8
+
9
+
10
+ class MemoryClassificationElement (ClassificationElement): # lgtm [py/missing-equals]
11
+ """
12
+ In-memory representation of classification results. This is represented
13
+ with a python dictionary.
14
+
15
+ :param type_name: Name of the type of classifier this classification
16
+ was generated by.
17
+ :type type_name: str
18
+
19
+ :param uuid: Unique ID reference of the classification
20
+ :type uuid: collections.abc.Hashable
21
+ """
22
+
23
+ __slots__ = ('_c', '_c_lock')
24
+
25
+ def __init__(self, type_name: str, uuid: Hashable):
26
+ super(MemoryClassificationElement, self).__init__(type_name, uuid)
27
+
28
+ # dictionary of classification labels and values
29
+ self._c: Optional[CLASSIFICATION_DICT_T] = None
30
+ # Cannot be pickled. New lock initialized upon pickle/unpickle
31
+ self._c_lock = RLock()
32
+
33
+ @classmethod
34
+ def is_usable(cls) -> bool:
35
+ # No external dependencies
36
+ return True
37
+
38
+ def get_config(self) -> Dict[str, Any]:
39
+ return {}
40
+
41
+ def __getstate__(self) -> Any:
42
+ state = {
43
+ 'parent': super(MemoryClassificationElement, self).__getstate__(),
44
+ }
45
+ with self._c_lock:
46
+ state['c'] = self._c
47
+ return state
48
+
49
+ def __setstate__(self, state: Any) -> None:
50
+ super(MemoryClassificationElement, self).__setstate__(state['parent'])
51
+ if not hasattr(self, '_c_lock') or self._c_lock is None:
52
+ self._c_lock = RLock()
53
+ with self._c_lock:
54
+ #: :type: None | dict[collections.abc.Hashable, float]
55
+ self._c = state['c']
56
+
57
+ def has_classifications(self) -> bool:
58
+ with self._c_lock:
59
+ return bool(self._c)
60
+
61
+ def get_classification(self) -> CLASSIFICATION_DICT_T:
62
+ with self._c_lock:
63
+ if self._c:
64
+ return self._c
65
+ else:
66
+ raise NoClassificationError("No classification labels/values")
67
+
68
+ def set_classification(
69
+ self,
70
+ m: Optional[CLASSIFICATION_MAP_T] = None,
71
+ **kwds: float
72
+ ) -> CLASSIFICATION_DICT_T:
73
+ m = super(MemoryClassificationElement, self)\
74
+ .set_classification(m, **kwds)
75
+ with self._c_lock:
76
+ self._c = m
77
+ return m