hdlib 2.1.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.
hdlib/space.py ADDED
@@ -0,0 +1,864 @@
1
+ """Implementation of hyperdimensional Space.
2
+
3
+ __hdlib__ provides the _Space_ class under _hdlib.space_ for building the abstract representation of
4
+ a hyperdimensional space which acts as a container for a multitude of vectors."""
5
+
6
+ import errno
7
+ import os
8
+ import pickle
9
+ from collections import OrderedDict
10
+ from typing import List, Optional, Set, Tuple, Union
11
+
12
+ import numpy as np
13
+
14
+ from hdlib import __version__
15
+ from hdlib.vector import Vector
16
+
17
+
18
+ class Space(object):
19
+ """Vectors space."""
20
+
21
+ def __init__(self, size: int=10000, vtype: str="bipolar", from_file: Optional[os.path.abspath]=None) -> "Space":
22
+ """Initialize the vectors space as a dictionary of Vector objects.
23
+
24
+ Parameters
25
+ ----------
26
+ size : int, optional, default 10000
27
+ Size of vectors in the space.
28
+ vtype : {'binary', 'bipolar'}, default 'bipolar'
29
+ The type of vectors in space.
30
+ from_file : str, default None
31
+ Path to a pickle file. Used to load a Space object from file.
32
+
33
+ Returns
34
+ -------
35
+ Space
36
+ A new Space object.
37
+
38
+ Raises
39
+ ------
40
+ Exception
41
+ If the pickle object in `from_file` is not instance of Space.
42
+ FileNotFoundError
43
+ If `from_file` is not None but the file does not exist.
44
+ ValueError
45
+ If `vtype` is different than 'binary' or 'bipolar'.
46
+
47
+ Examples
48
+ --------
49
+ >>> from hdlib.space import Space
50
+ >>> space = Space()
51
+ <class 'hdlib.space.Space'>
52
+
53
+ Create a Space object that can host bipolar vectors with a size of 10,000 by default.
54
+
55
+ >>> Space(size=10)
56
+ ValueError: Size of vectors in space must be greater than or equal to 1000
57
+
58
+ This throws a ValueError since the vector size cannot be less than 1,000.
59
+
60
+ >>> space1 = Space()
61
+ >>> space1.dump(to_file='~/my_space.pkl')
62
+ >>> space2 = Space(from_file='~/my_space.pkl')
63
+ >>> type(space2)
64
+ <class 'hdlib.space.Space'>
65
+
66
+ This creates an empty space `space1`, dumps the object to a pickle file under the home directory,
67
+ and finally create a new space object `space2` from the pickle file.
68
+ """
69
+
70
+ # We may want to iterate over the Space object
71
+ # Thus, we need to maintain the order of the vectors into the space dictionary
72
+ self.space = OrderedDict()
73
+
74
+ # Used to iterate over vectors in the space
75
+ self._vector_index = 0
76
+
77
+ self.version = __version__
78
+
79
+ self.size = size
80
+
81
+ self.vtype = vtype.lower()
82
+
83
+ if self.vtype not in ("binary", "bipolar"):
84
+ raise ValueError("Vector type not supported")
85
+
86
+ self.tags = dict()
87
+
88
+ # Vector links can be used to define a tree structure
89
+ # Use this flag to mark a vector as root
90
+ self.root = None
91
+
92
+ if from_file:
93
+ if not os.path.isfile(from_file):
94
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), from_file)
95
+
96
+ else:
97
+ with open(from_file, "rb") as pkl:
98
+ from_file_obj = pickle.load(pkl)
99
+
100
+ if not isinstance(from_file_obj, type(self)):
101
+ raise Exception("Pickle object is not instance of {}".format(type(self)))
102
+
103
+ self.__dict__.update(from_file_obj.__dict__)
104
+
105
+ if self.version != __version__:
106
+ print("Warning: the specified Space has been created with a different version of hdlib")
107
+
108
+ def __iter__(self) -> "Space":
109
+ """Required to make the Space object iterable."""
110
+
111
+ return self
112
+
113
+ def __next__(self) -> str:
114
+ """Used to iterate over the vector objects into the Space.
115
+
116
+ Returns
117
+ -------
118
+ str
119
+ The vector name at a specific position.
120
+ """
121
+
122
+ if self._vector_index >= len(self.space):
123
+ # Set the vector index back to the first position.
124
+ # Redy to start iterating again over the vectors in the space
125
+ self._vector_index = 0
126
+
127
+ raise StopIteration
128
+
129
+ else:
130
+ # Retrieve the vector name at a specific position in the space
131
+ # Vectors are all ordered in the space since the space is defined as an OrderedDict
132
+ vector = self.memory()[self._vector_index]
133
+
134
+ # Increment the vector index for the next iteration
135
+ self._vector_index += 1
136
+
137
+ # This returns the vector name or ID
138
+ # It is enough, since the space is a hashmap and we can retrieve the Vector object in O(1)
139
+ return vector
140
+
141
+ def __contains__(self, vector: str) -> bool:
142
+ """Check whether a vector is in the space.
143
+
144
+ Parameters
145
+ ----------
146
+ vector : str
147
+ The vector name or ID.
148
+
149
+ Returns
150
+ -------
151
+ bool
152
+ True if `vector` is in the space, False otherwise.
153
+
154
+ Examples
155
+ --------
156
+ >>> from hdlib.space import Space, Vector
157
+ >>> space = Space()
158
+ >>> vector = Vector(name="my_vector")
159
+ >>> space.insert(vector)
160
+ >>> "my_vector" in space
161
+ True
162
+
163
+ Create a Space object, add a Vector object into the space, and check whether the
164
+ vector is actually in the space by searching for its name.
165
+ """
166
+
167
+ return vector in self.space
168
+
169
+ def __len__(self) -> int:
170
+ """Get the number of vectors in space.
171
+
172
+ Returns
173
+ -------
174
+ int
175
+ The number of vectors in space.
176
+
177
+ Examples
178
+ --------
179
+ >>> from hdlib.space import Space, Vector
180
+ >>> space = Space()
181
+ >>> vector = Vector()
182
+ >>> space.insert(vector)
183
+ >>> len(space)
184
+ 1
185
+
186
+ Create a Space object, add a Vector object into the space, and check the total number
187
+ of Vector objects in the space.
188
+ """
189
+
190
+ return len(self.space)
191
+
192
+ def __str__(self) -> str:
193
+ """Print the Space object properties.
194
+
195
+ Returns
196
+ -------
197
+ str
198
+ A description of the Space object. It reports the size, vector type,
199
+ the number of vectors in space, the set of vectors tags, and the set of vectors names.
200
+
201
+ Examples
202
+ --------
203
+ >>> from hdlib.space import Space, Vector
204
+ >>> space = Space()
205
+ >>> vector = Vector(name='my_vector')
206
+ >>> space.insert(vector)
207
+ >>> print(space)
208
+
209
+ Class: hdlib.space.Space
210
+ Version: 0.1.17
211
+ Size: 10000
212
+ Type: bipolar
213
+ Vectors: 1
214
+ Tags:
215
+
216
+ []
217
+
218
+ IDs:
219
+
220
+ ['my_vector']
221
+
222
+ Print the Space object properties. It contains only one vector.
223
+ The vector size and type are 10,000 and 'bipolar' by default.
224
+ """
225
+
226
+ return f"""
227
+ Class: hdlib.space.Space
228
+ Version: {self.version}
229
+ Size: {self.size}
230
+ Type: {self.vtype}
231
+ Vectors: {len(self.space)}
232
+ Tags:
233
+
234
+ {np.array(list(self.tags.keys()))}
235
+
236
+ IDs:
237
+
238
+ {np.array(list(self.space.keys()))}
239
+ """
240
+
241
+ def memory(self) -> List[str]:
242
+ """Return names or IDs of vectors in space.
243
+
244
+ Returns
245
+ -------
246
+ list
247
+ A list with vectors names or IDs
248
+
249
+ Examples
250
+ --------
251
+ >>> from hdlib.space import Space, Vector
252
+ >>> space = Space()
253
+ >>> vector = Vector(name='my_vector')
254
+ >>> space.insert(vector)
255
+ >>> space.memory()
256
+ ['my_vector']
257
+
258
+ Create a Space and add a Vector called 'my_vector'. The memory function returns
259
+ the list of vector names. In this case a list with one element only 'my_vector'.
260
+ """
261
+
262
+ return list(self.space.keys())
263
+
264
+ def get(
265
+ self,
266
+ names: Optional[List[str]]=None,
267
+ tags: Optional[List[Union[str, int, float]]]=None
268
+ ) -> List[Vector]:
269
+ """Get vectors by names or tags.
270
+
271
+ Parameters
272
+ ----------
273
+ names : list, optional
274
+ A list with vector names. It is required in case no tags are specified.
275
+ tags : list, optional
276
+ A list with vector tags. It is required in case no names are specified.
277
+
278
+ Returns
279
+ -------
280
+ list
281
+ A list of Vector objects in the space according to the specified names or tags.
282
+
283
+ Raises
284
+ ------
285
+ Exception
286
+ - if no `names` or `tags` are provided in input;
287
+ - if both `names` and `tags` are provided in input.
288
+ TypeError
289
+ If names or tags in the input lists are not instance of primitives.
290
+
291
+ Examples
292
+ --------
293
+ >>> from hdlib.space import Space, Vector
294
+ >>> space = Space()
295
+ >>> vector1 = Vector(name='my_vector_1', tags={'tag1', 'tag2'})
296
+ >>> vector2 = Vector(name='my_vector_2', tags={'tag2', 'tag3', 'tag4'})
297
+ >>> space.insert(vector1)
298
+ >>> space.insert(vector2)
299
+ >>> vectors = space.get(tags=['tag2'])
300
+ >>> for vector in vectors:
301
+ ... print(vector.name)
302
+ my_vector_1
303
+ my_vector_2
304
+
305
+ This creates two Vector objects with a few tags and add them to a Space.
306
+ It then retrieves a list of vectors by searching for a specific tag which is in common between
307
+ the two vectors in this case. It finally prints the vector names.
308
+ """
309
+
310
+ if not names and not tags:
311
+ raise Exception("No names or tags provided")
312
+
313
+ if names and tags:
314
+ raise Exception("Cannot search for vectors by their names and tags at the same time")
315
+
316
+ vectors = set()
317
+
318
+ if names:
319
+ try:
320
+ names = [str(name) for name in names]
321
+
322
+ except:
323
+ raise TypeError("Vector name must be instance of a primitive")
324
+
325
+ for vector_name in names:
326
+ if vector_name in self.space:
327
+ vectors.add(self.space[vector_name])
328
+
329
+ elif tags:
330
+ for tag in tags:
331
+ if not isinstance(tag, str) and not isinstance(tag, int) and not isinstance(tag, float):
332
+ raise TypeError("A tags must be string, integer, or float")
333
+
334
+ if tag in self.tags:
335
+ for vector_name in self.tags[tag]:
336
+ vectors.add(self.space[vector_name])
337
+
338
+ return list(vectors)
339
+
340
+ def insert(self, vector: Vector) -> None:
341
+ """Add a Vector object to the space.
342
+
343
+ Parameters
344
+ ----------
345
+ vector : Vector
346
+ The input Vector object that must be added to the Space
347
+
348
+ Raises
349
+ ------
350
+ Exception
351
+ - if the vector size or type is not compatible with the space;
352
+ - if a vector with the same name of the input one is already in the space.
353
+
354
+ Examples
355
+ --------
356
+ >>> from hdlib.space import Space, Vector
357
+ >>> vector = Vector()
358
+ >>> space = Space()
359
+ >>> space.insert(vector)
360
+
361
+ It creates a random bipolar vector with size 10,000 and adds it to a space that by default can host
362
+ bipolar vectors with size 10,000.
363
+
364
+ >>> vector = Vector(size=15000)
365
+ >>> space = Space()
366
+ >>> space.insert(vector)
367
+ Exception: Space and vectors with different size are not compatible
368
+
369
+ By default, the space can host bipolar vectors with size 10,000, while here we explicitly created a
370
+ Vector object with size 15,000 which is not compatible with the space.
371
+ """
372
+
373
+ if self.size != vector.size:
374
+ raise Exception("Space and vectors with different size are not compatible")
375
+
376
+ if self.vtype != vector.vtype:
377
+ raise Exception("Attempting to insert a {} vector into a {} space: failed".format(vector.vtype, self.vtype))
378
+
379
+ if vector.name in self.space:
380
+ raise Exception("Vector \"{}\" already in space".format(vector.name))
381
+
382
+ self.space[vector.name] = vector
383
+
384
+ for tag in vector.tags:
385
+ if tag not in self.tags:
386
+ self.tags[tag] = set()
387
+
388
+ self.tags[tag].add(vector.name)
389
+
390
+ def bulk_insert(
391
+ self,
392
+ names: List[str],
393
+ tags: Optional[List[List[Union[str, int, float]]]]=None,
394
+ ignore_existing: bool=False
395
+ ) -> None:
396
+ """Add vectors to the space in bulk.
397
+
398
+ Parameters
399
+ ----------
400
+ names : list
401
+ A list with vector names.
402
+ tags : list, optional
403
+ An optional list of lists with vector tags.
404
+ ignore_existing : bool, default False
405
+ If True, do not raise an exception in case the space contains a vector with the same name specified in `names`.
406
+
407
+ Raises
408
+ ------
409
+ TypeError
410
+ - if `names` or `tags` are not instance of list;
411
+ - if the elements of the `names` list are not instance of a primitive.
412
+ Exception
413
+ - if the number of elements in `names` doesn't match with the number of elements in `tags`;
414
+ - if there already a vector in the space with the same name in `names`.
415
+
416
+ Examples
417
+ --------
418
+ >>> from hdlib.space import Space
419
+ >>> space = Space()
420
+ >>> space.bulk_insert(names=['my_vector_1', 'my_vector_2'])
421
+ >>> space.memory()
422
+ ['my_vector_1', 'my_vector_2']
423
+
424
+ Create two random bipolar vectors with size 10,000 just by specifying a list with vector names.
425
+ The vector type and size is inherited by the space that by default can host bipolar vectors with size 10,000.
426
+
427
+ >>> space.bulk_insert(names=['my_vector_3', 'my_vector_4'], tags=[['tag1'], ['tag1', 'tag2']])
428
+ >>> vectors = space.get(tags=['tag1'])
429
+ >>> for vector in vectors:
430
+ ... print(vector.name)
431
+ my_vector_3
432
+ my_vector_4
433
+
434
+ Add other two vectors and assigned them a few tags, then retrieve the vectors with tag 'tag1'.
435
+ Both 'my_vector_3' and 'my_vector_4' contain 'tag1' in their set of tags.
436
+ """
437
+
438
+ if not isinstance(names, list):
439
+ raise TypeError("Input must be a list of strings")
440
+
441
+ if tags and not isinstance(tags, list):
442
+ raise TypeError("tags must be a list of lists of strings")
443
+
444
+ if tags and len(names) != len(tags):
445
+ raise Exception("The number of vector IDs must match the size of the tags list")
446
+
447
+ names = set(names)
448
+
449
+ for pos, name in enumerate(names):
450
+ if not isinstance(name, (bool, str, int, float, None)):
451
+ raise TypeError("Entries in input list must be instances of primitives")
452
+
453
+ name = str(name)
454
+
455
+ if name in self.space:
456
+ if not ignore_existing:
457
+ raise Exception("Vector \"{}\" already exists in the space".format(name))
458
+
459
+ else:
460
+ continue
461
+
462
+ vector_tags = set(tags[pos]) if tags else set()
463
+
464
+ vector = Vector(name=name, size=self.size, tags=vector_tags, vtype=self.vtype)
465
+
466
+ self.insert(vector)
467
+
468
+ def remove(self, name: str) -> Vector:
469
+ """Remove a vector from the space by its name.
470
+
471
+ Parameters
472
+ ----------
473
+ name : str
474
+ The name of the vector that must be removed from the space.
475
+
476
+ Returns
477
+ -------
478
+ Vector
479
+ Returns the Vector object.
480
+
481
+ Raises
482
+ ------
483
+ TypeError
484
+ If the vector name is not an instance of a primitive.
485
+ Exception
486
+ If there is not a vector with that specific name in the space.
487
+
488
+ Examples
489
+ --------
490
+ >>> form hdlib.space import Space, Vector
491
+ >>> vector = Vector(name='my_vector')
492
+ >>> space = Space()
493
+ >>> space.insert(vector)
494
+ >>> space.remove('my_vector')
495
+ >>> len(space)
496
+ 0
497
+
498
+ Create a vector called 'my_vector', add it to the space and then remove it.
499
+ Finally check how many vectors are in the space.
500
+ """
501
+
502
+ try:
503
+ name = str(name)
504
+
505
+ except:
506
+ raise TypeError("Vector name must be instance of a primitive")
507
+
508
+ if name not in self.space:
509
+ raise Exception("Vector not in space")
510
+
511
+ vector = self.space[name]
512
+
513
+ del self.space[name]
514
+
515
+ for tag in vector.tags:
516
+ self.tags[tag].remove(vector.name)
517
+
518
+ if not self.tags[tag]:
519
+ del self.tags[tag]
520
+
521
+ return vector
522
+
523
+ def add_tag(self, name: str, tag: Union[str, int, float]) -> None:
524
+ """Tag a vector.
525
+
526
+ Parameters
527
+ ----------
528
+ name : str
529
+ The vector name or ID.
530
+ tag : str, int, float
531
+ The tag must be a primitive.
532
+
533
+ Raises
534
+ ------
535
+ TypeError
536
+ If the name or tag are not instance of primitives.
537
+ Exception
538
+ If there is not a vector in the space with that specific name or ID.
539
+
540
+ Examples
541
+ --------
542
+ >>> from hdlib.space import Space, Vector
543
+ >>> space = Space()
544
+ >>> my_vector = Vector(name='my_vector')
545
+ >>> space.insert(my_vector)
546
+ >>> space.add_tag('my_vector', 'tag')
547
+ >>> for vector in space.get(tags['tag']):
548
+ ... print(vector.name)
549
+ my_vector
550
+
551
+ This creates a Vector object add it to a Space. It then assigns a tag to the vector and searches
552
+ for vector with that specific tag within the space. It finally prints the vector names.
553
+ """
554
+
555
+ try:
556
+ name = str(name)
557
+
558
+ except:
559
+ raise TypeError("Vector name must be instance of a primitive")
560
+
561
+ if name not in self.space:
562
+ raise Exception("Vector not in space")
563
+
564
+ if not isinstance(tag, str) and not isinstance(tag, int) and not isinstance(tag, float):
565
+ raise TypeError("Tags must be string, integer, or float")
566
+
567
+ self.space[name].tags.add(tag)
568
+
569
+ if tag not in self.tags:
570
+ self.tags[tag] = set()
571
+
572
+ self.tags[tag].add(name)
573
+
574
+ def remove_tag(self, name: str, tag: Union[str, int, float]) -> None:
575
+ """Untag a vector.
576
+
577
+ Parameters
578
+ ----------
579
+ name : str
580
+ The vector name or ID.
581
+ tag : str, int, float
582
+ The tag must be a primitive.
583
+
584
+ Raises
585
+ ------
586
+ TypeError
587
+ If the name or tag are not instance of primitives.
588
+ Exception
589
+ If there is not a vector in the space with that specific name or ID.
590
+
591
+ Examples
592
+ --------
593
+ >>> from hdlib.space import Space, Vector
594
+ >>> space = Space()
595
+ >>> my_vector = Vector(name='my_vector', tags={'tag'})
596
+ >>> space.insert(my_vector)
597
+ >>> space.remove_tag('my_vector', 'tag')
598
+ >>> len(space.get(tags['tag']))
599
+ 0
600
+
601
+ This initializes a space, inserts a vector with a tag into the space, then untags the vector, and
602
+ finally searches for vectors with that specific tag. No vectors are returned since there was only
603
+ one vector with that tag that has been untagged.
604
+ """
605
+
606
+ try:
607
+ name = str(name)
608
+
609
+ except:
610
+ raise TypeError("Vector name must be instance of a primitive")
611
+
612
+ if name not in self.space:
613
+ raise Exception("Vector not in space")
614
+
615
+ if not isinstance(tag, str) and not isinstance(tag, int) and not isinstance(tag, float):
616
+ raise TypeError("Tags must be string, integer, or float")
617
+
618
+ if tag in self.tags:
619
+ self.space[name].tags.remove(tag)
620
+
621
+ self.tags[tag].remove(name)
622
+
623
+ if not self.tags[tag]:
624
+ del self.tags[tag]
625
+
626
+ def link(self, name1: str, name2: str) -> None:
627
+ """Link two vectors in the space through by their names. Links are directed edges.
628
+
629
+ Parameters
630
+ ----------
631
+ name1 : str
632
+ Name or ID of the first vector.
633
+ name2 : str
634
+ Name or ID of the second vector.
635
+
636
+ Raises
637
+ ------
638
+ TypeError
639
+ If vectors names are not instance of a primitive.
640
+ Exception
641
+ If there are no vectors in space named `name1` and `name2`.
642
+
643
+ Examples
644
+ --------
645
+ >>> from hdlib.space import Space, Vector
646
+ >>> space = Space()
647
+ >>> vector1 = Vector(name='vector1')
648
+ >>> vector2 = Vector(name='vector2')
649
+ >>> space.insert(vector1)
650
+ >>> space.insert(vector2)
651
+ >>> space.link('vector2', 'vector1')
652
+ >>> vector2 = space.get(names=['vector2'])[0]
653
+ >>> 'vector1' in vector2.children
654
+ True
655
+
656
+ Define a space with two vectors 'vector1' and 'vector2'. Link 'vector2' with 'vector1'.
657
+ Retrieve 'vector2' from the space and check whether 'vector1' is in its set of linked nodes.
658
+ """
659
+
660
+ try:
661
+ name1 = str(name1)
662
+
663
+ name2 = str(name2)
664
+
665
+ except:
666
+ raise TypeError("Vector name must be instance of a primitive")
667
+
668
+ if name1 not in self.space:
669
+ raise Exception("Vector \"{}\" not in space".format(name1))
670
+
671
+ if name2 not in self.space:
672
+ raise Exception("Vector \"{}\" not in space".format(name2))
673
+
674
+ self.space[name1].children.add(name2)
675
+
676
+ self.space[name2].parents.add(name1)
677
+
678
+ def set_root(self, name: str) -> None:
679
+ """Vector links can be used to define a tree structure. Set a specific vector as root.
680
+
681
+ Parameters
682
+ ----------
683
+ name : str
684
+ Name or ID of vector in space.
685
+
686
+ Raises
687
+ ------
688
+ TypeError
689
+ If the vector name or ID is not instance of a primitive.
690
+ Exception
691
+ If there are no vectors in the space with the specified name.
692
+
693
+ Examples
694
+ --------
695
+ >>> from hdlib.space import Space
696
+ >>> space = Space()
697
+ >>> space.bulk_insert(names=['vector1', 'vector2', 'vector3'])
698
+ >>> space.link('vector1', 'vector2')
699
+ >>> space.link('vector1', 'vector3')
700
+ >>> space.set_root('vector1')
701
+ >>> vector1 = space.get(names=['vector1'])[0]
702
+ >>> for vector in vector1.children:
703
+ ... print(vector)
704
+ vector2
705
+ vector3
706
+
707
+ Create a space and add three vectors in bulk. Link 'vector1' to 'vector2' and 'vector3', and
708
+ set 'vector1' as root. Finally, print the name of the nodes linked to the root.
709
+ """
710
+
711
+ try:
712
+ name = str(name)
713
+
714
+ except:
715
+ raise TypeError("Vector name must be instance of a primitive")
716
+
717
+ if name not in self.space:
718
+ raise Exception("Vector \"{}\" not in space".format(name))
719
+
720
+ self.root = name
721
+
722
+ def find(self, vector: Vector, threshold: float=np.inf, method: str="cosine") -> Tuple[str, float]:
723
+ """Search for the closest vector in space.
724
+
725
+ Parameters
726
+ ----------
727
+ vector : Vector
728
+ Input Vector object. Search for the closest vector to this Vector in the space.
729
+ threshold : float, default numpy.Inf
730
+ Threshold on distance between vectors.
731
+ method : {'cosine', 'euclidean', 'hamming'}, default 'cosine'
732
+ Distance metric.
733
+
734
+ Returns
735
+ -------
736
+ tuple
737
+ A tuple with the name of the closest vector in space and its distance with the input vector.
738
+
739
+ Examples
740
+ --------
741
+ >>> from hdlib.space import Space, Vector
742
+ >>> space = Space()
743
+ >>> vector1 = Vector(name='vector1')
744
+ >>> vector2 = Vector(name='vector2')
745
+ >>> vector3 = Vector(name='vector3')
746
+ >>> space.insert(vector1)
747
+ >>> space.insert(vector2)
748
+ >>> space.insert(vector3)
749
+ >>> space.find(vector1)
750
+ ('vector1', 0.0)
751
+
752
+ Create a space with three vectors 'vector1', 'vector2', and 'vector3', and search for the closest vector to 'vector1'.
753
+ The result is obviously itself, 'vector1', with a cosine distance of 0.0.
754
+ """
755
+
756
+ # Exploit self.find_all() to seach for the best match
757
+ # It will take care of raising exceptions in case of problems with input arguments
758
+ distances, best = self.find_all(vector, threshold=threshold, method=method)
759
+
760
+ return best, distances[best]
761
+
762
+ def find_all(self, vector: Vector, threshold: float=np.inf, method: str="cosine") -> Tuple[dict, str]:
763
+ """Compute distance of the input vector against all vectors in space.
764
+
765
+ Parameters
766
+ ----------
767
+ vector : Vector
768
+ Input Vector object. Search for the closest vector to this Vector in the space.
769
+ threshold : float, default numpy.Inf
770
+ Threshold on distance between vectors.
771
+ method : {'cosine', 'euclidean', 'hamming'}, default 'cosine'
772
+ Distance metric.
773
+
774
+ Returns
775
+ -------
776
+ dict
777
+ A dictionary the distances between the input vector and all the other vectors in the space,
778
+ in addition to the name of the closest vector.
779
+
780
+ Raises
781
+ ------
782
+ ValueError
783
+ If the threshold is lower than 0.0.
784
+ Exception
785
+ If the size of the input vector is not compatible with the size of vectors in the space.
786
+
787
+ Examples
788
+ --------
789
+ >>> from hdlib.space import Space, Vector
790
+ >>> space = Space()
791
+ >>> vector1 = Vector(name='vector1', seed=1)
792
+ >>> vector2 = Vector(name='vector2', seed=2)
793
+ >>> vector3 = Vector(name='vector3', seed=3)
794
+ >>> space.insert(vector1)
795
+ >>> space.insert(vector2)
796
+ >>> space.insert(vector3)
797
+ >>> space.find_all(vector1)
798
+ ({'vector1': 0.0, 'vector2': 0.996, 'vector3': 0.985}, 'vector1')
799
+
800
+ Create a space with three vectors 'vector1', 'vector2', and 'vector3', and compute the cosine distance between 'vector1'
801
+ and all the other vectors in space (including itseld). The closest vector is obviously itself, 'vector1', with a cosine
802
+ distance of 0.0. Use a seed for reproducing the same distances.
803
+ """
804
+
805
+ if self.size != vector.size:
806
+ raise Exception("Space and vectors with different size are not compatible")
807
+
808
+ if threshold < 0.0:
809
+ raise ValueError("Threshold cannot be lower than 0.0")
810
+
811
+ distances = dict()
812
+
813
+ distance = np.inf
814
+
815
+ best = None
816
+
817
+ for v in self.space:
818
+ # Compute distance
819
+ dist = self.space[v].dist(vector, method=method)
820
+
821
+ if dist <= threshold:
822
+ distances[v] = dist
823
+
824
+ if distances[v] < distance:
825
+ best = v
826
+
827
+ distance = distances[v]
828
+
829
+ return distances, best
830
+
831
+ def dump(self, to_file: Optional[os.path.abspath]=None) -> None:
832
+ """Dump the Space object to a pickle file.
833
+
834
+ Parameters
835
+ ----------
836
+ to_file
837
+ Path to the file used to dump the Space object to.
838
+
839
+ Raises
840
+ ------
841
+ Exception
842
+ If the `to_file` file already exists.
843
+
844
+ Examples
845
+ --------
846
+ >>> import os
847
+ >>> from hdlib.space import Space
848
+ >>> space = Space()
849
+ >>> space.dump(to_file='~/my_space.pkl')
850
+ >>> os.path.isfile('~/my_space.pkl')
851
+ True
852
+
853
+ Create a Space object and dump it to a pickle file under the home directory.
854
+ """
855
+
856
+ if not to_file:
857
+ # Dump the space to a pickle file in the current working directory if not file path is provided
858
+ to_file = os.path.join(os.getcwd(), "space.pkl")
859
+
860
+ if os.path.isfile(to_file):
861
+ raise Exception("The output file already exists!\n{}".format(to_file))
862
+
863
+ with open(to_file, "wb") as pkl:
864
+ pickle.dump(self, pkl)