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/vector.py ADDED
@@ -0,0 +1,608 @@
1
+ """Implementation of hyperdimensional Vector.
2
+
3
+ __hdlib__ provides the _Vector_ class under _hdlib.vector_ for building the abstract representation of hyperdimensional vectors."""
4
+
5
+ import errno
6
+ import os
7
+ import pickle
8
+ import uuid
9
+ from typing import Optional, Set, Union
10
+
11
+ import numpy as np
12
+
13
+ from hdlib import __version__
14
+
15
+
16
+ class Vector(object):
17
+ """Vector object."""
18
+
19
+ def __init__(
20
+ self,
21
+ name: Optional[str]=None,
22
+ size: int=10000,
23
+ vector: Optional[np.ndarray]=None,
24
+ vtype: str="bipolar",
25
+ tags: Optional[Set[Union[str, int, float]]]=None,
26
+ seed: Optional[int]=None,
27
+ warning: bool=False,
28
+ from_file: Optional[os.path.abspath]=None,
29
+ ) -> "Vector":
30
+ """Initialize a Vector object.
31
+
32
+ Parameters
33
+ ----------
34
+ name : str, optional
35
+ The unique identifier of the Vector object. A random UUID v4 is generated if not specified.
36
+ size : int, optional, default 10000
37
+ The size of the vector. It is 10,000 by default.
38
+ vector : numpy.ndarray, optional, default None
39
+ The actual vector. A random vector is created if not specified.
40
+ vtype : {'binary', 'bipolar'}, default 'bipolar'
41
+ The vector type.
42
+ tags : set, default None
43
+ An optional set of vector tags. Tags can be str, int, and float.
44
+ seed : int, default None
45
+ An optional seed for reproducibly generating the vector numpy.ndarray randomly.
46
+ warning : bool, default False
47
+ Print warning messages if True.
48
+ from_file : str, default None
49
+ Path to a pickle file. Used to load a Vector object from file.
50
+
51
+ Returns
52
+ -------
53
+ Vector
54
+ A new Vector object.
55
+
56
+ Raises
57
+ ------
58
+ Exception
59
+ If the pickle object in `from_file` is not instance of Vector.
60
+ FileNotFoundError
61
+ If `from_file` is not None but the file does not exist.
62
+ TypeError
63
+ - if the vector name is not instance of a primitive;
64
+ - if `tags` is not an instance of set;
65
+ - if `vector` is not an instance of numpy.ndarray;
66
+ - if `size` is not an integer number.
67
+ ValueError
68
+ If `vtype` is different than 'binary' or 'bipolar'.
69
+
70
+ Examples
71
+ --------
72
+ >>> from hdlib.vector import Vector
73
+ >>> vector = Vector()
74
+ >>> type(vector)
75
+ <class 'hdlib.vector.Vector'>
76
+
77
+ A new bipolar vector with a size of 1,000 is created by default.
78
+
79
+ >>> vector1 = Vector()
80
+ >>> vector1.dump(to_file='~/my_vector.pkl')
81
+ >>> vector2 = Vector(from_file='~/my_vector.pkl')
82
+ >>> type(vector2)
83
+ <class 'hdlib.vector.Vector'>
84
+
85
+ This creates a random bipolar vector `vector1`, dumps the object to a pickle file under the home directory,
86
+ and finally create a new vector object `vector2` from the pickle file.
87
+ """
88
+
89
+ # Conditions on vector name or ID
90
+ # Vector name is casted to string. For this reason, only Python primitives are allowed
91
+ # A random name is assigned if not specified
92
+ try:
93
+ if name is None:
94
+ name = str(uuid.uuid4())
95
+
96
+ else:
97
+ name = str(name)
98
+
99
+ self.name = name
100
+
101
+ except:
102
+ raise TypeError("Vector name must be instance of a primitive")
103
+
104
+ # Register random seed for reproducibility
105
+ self.seed = seed
106
+
107
+ # Take track of the hdlib version
108
+ self.version = __version__
109
+
110
+ if tags and not isinstance(tags, set):
111
+ raise TypeError("Tags must be a set")
112
+
113
+ # Add tags
114
+ self.tags = tags if tags else set()
115
+
116
+ # Add links
117
+ # Used to link Vectors by their names or IDs
118
+ self.parents = set()
119
+ self.children = set()
120
+
121
+ # Conditions on vector
122
+ # It must be a numpy.ndarray
123
+ # A random vector is generated if not specified
124
+ if vector is not None:
125
+ if not isinstance(vector, np.ndarray):
126
+ raise TypeError("Vector must be instance of numpy.ndarray")
127
+
128
+ self.vector = vector
129
+
130
+ self.size = len(self.vector)
131
+
132
+ self.vtype = vtype
133
+
134
+ elif from_file:
135
+ if not os.path.isfile(from_file):
136
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), from_file)
137
+
138
+ else:
139
+ # Load vector from pickle file
140
+ with open(from_file, "rb") as pkl:
141
+ from_file_obj = pickle.load(pkl)
142
+
143
+ if not isinstance(from_file_obj, type(self)):
144
+ raise Exception("Pickle object is not instance of {}".format(type(self)))
145
+
146
+ self.__dict__.update(from_file_obj.__dict__)
147
+
148
+ if self.version != __version__:
149
+ print("Warning: the specified Vector has been created with a different version of hdlib")
150
+
151
+ else:
152
+ if not isinstance(size, int):
153
+ raise TypeError("Vector size must be an integer number")
154
+
155
+ self.size = size
156
+
157
+ if vtype not in ("bipolar", "binary"):
158
+ raise ValueError("Vector type can be binary or bipolar only")
159
+
160
+ # Add vector type
161
+ self.vtype = vtype.lower()
162
+
163
+ if seed is None:
164
+ rand = np.random.default_rng()
165
+
166
+ else:
167
+ # Conditions on random seed for reproducibility
168
+ # numpy allows integers as random seeds
169
+ if not isinstance(seed, int):
170
+ raise TypeError("Seed must be an integer number")
171
+
172
+ rand = np.random.default_rng(seed=self.seed)
173
+
174
+ """
175
+ # Use the following instructions to generate random vectors with real numbers
176
+ self.vector = rand.uniform(low=-1.0, high=1.0, size=(self.size,))
177
+ self.vector /= np.linalg.norm(self.vector)
178
+ """
179
+
180
+ # Build a random binary vector
181
+ self.vector = rand.integers(2, size=size)
182
+
183
+ if vtype == "bipolar":
184
+ # Build a random bipolar vector
185
+ self.vector = 2 * self.vector - 1
186
+
187
+ def __len__(self) -> int:
188
+ """Get the vector size.
189
+
190
+ Returns
191
+ -------
192
+ int
193
+ The vector size.
194
+
195
+ Examples
196
+ --------
197
+ >>> from hdlib.vector import Vector
198
+ >>> vector = Vector()
199
+ >>> len(vector)
200
+ 10000
201
+
202
+ Return the vector size, which is 10,000 by default here
203
+ """
204
+
205
+ return self.size
206
+
207
+ def __str__(self) -> str:
208
+ """Print the Vector object properties.
209
+
210
+ Returns
211
+ -------
212
+ str
213
+ A description of the Vector object. It reports the name, seed, size,
214
+ vector type, tags, and the actual vector.
215
+
216
+ Examples
217
+ --------
218
+ >>> from hdlib.vector import Vector
219
+ >>> vector = Vector()
220
+ >>> print(vector)
221
+
222
+ Class: hdlib.vector.Vector
223
+ Version: 0.1.17
224
+ Name: 89ea628b-3d29-47e1-9d10-34bdbfce8d40
225
+ Seed: None
226
+ Size: 10000
227
+ Type: bipolar
228
+ Tags:
229
+
230
+ []
231
+
232
+ Vector:
233
+
234
+ [ 1 -1 -1 ... -1 1 -1]
235
+
236
+ Print the Vector object properties. The name has been generated as a UUID v4, while
237
+ the vector size and type are 10,000 and 'bipolar' by default. No tags have been specified.
238
+ Thus, the set of vector tags is empty.
239
+ """
240
+
241
+ return f"""
242
+ Class: hdlib.vector.Vector
243
+ Version: {self.version}
244
+ Name: {self.name}
245
+ Seed: {self.seed}
246
+ Size: {self.size}
247
+ Type: {self.vtype}
248
+ Tags:
249
+
250
+ {np.array(list(self.tags))}
251
+
252
+ Vector:
253
+
254
+ {self.vector}
255
+ """
256
+
257
+ def __add__(self, vector: "Vector") -> "Vector":
258
+ """Implement the addition operator between two Vector objects as bundle.
259
+
260
+ Returns
261
+ -------
262
+ Vector
263
+ A new vector object as the result of the bundle operator on the two input vectors.
264
+
265
+ Raises
266
+ ------
267
+ TypeError
268
+ If the input `vector` is not instance of the Vector class.
269
+
270
+ Examples
271
+ --------
272
+ >>> from hdlib.vector import Vector
273
+ >>> vector1 = Vector()
274
+ >>> vector2 = Vector()
275
+ >>> vector3 = vector1 + vector2
276
+ >>> type(vector3)
277
+ <class 'hdlib.vector.Vector'>
278
+
279
+ The bundle function returns a new Vector object whose content is computed as the element-wise sum
280
+ of the two input vectors.
281
+ """
282
+
283
+ if not isinstance(vector, type(self)):
284
+ raise TypeError("Cannot apply the bundle operator to non-Vector objects")
285
+
286
+ # Import arithmetic.bundle here to avoid circular imports
287
+ from hdlib.arithmetic import bundle as bundle_operator
288
+
289
+ return bundle_operator(self, vector)
290
+
291
+ def __sub__(self, vector: "Vector") -> "Vector":
292
+ """Implement the subtraction operator between two Vector objects.
293
+
294
+ Returns
295
+ -------
296
+ Vector
297
+ A new vector object as the result of the subtraction operator on the two input vectors.
298
+
299
+ Raises
300
+ ------
301
+ TypeError
302
+ If the input `vector` is not instance of the Vector class.
303
+
304
+ Examples
305
+ --------
306
+ >>> from hdlib.vector import Vector
307
+ >>> vector1 = Vector()
308
+ >>> vector2 = Vector()
309
+ >>> vector3 = vector1 - vector2
310
+ >>> type(vector3)
311
+ <class 'hdlib.vector.Vector'>
312
+
313
+ The subtraction operation returns a new Vector object whose content is computed as the element-wise
314
+ subtraction of the two input vectors.
315
+ """
316
+
317
+ if not isinstance(vector, type(self)):
318
+ raise TypeError("Cannot apply the subtraction operator to non-Vector objects")
319
+
320
+ # Import arithmetic.bind here to avoid circular imports
321
+ from hdlib.arithmetic import subtraction as subtraction_operator
322
+
323
+ return subtraction_operator(self, vector)
324
+
325
+ def __mul__(self, vector: "Vector") -> "Vector":
326
+ """Implement the multiplication operator between two Vector objects as bind.
327
+
328
+ Returns
329
+ -------
330
+ Vector
331
+ A new vector object as the result of the bind operator on the two input vectors.
332
+
333
+ Raises
334
+ ------
335
+ TypeError
336
+ If the input `vector` is not instance of the Vector class.
337
+
338
+ Examples
339
+ --------
340
+ >>> from hdlib.vector import Vector
341
+ >>> vector1 = Vector()
342
+ >>> vector2 = Vector()
343
+ >>> vector3 = vector1 * vector2
344
+ >>> type(vector3)
345
+ <class 'hdlib.vector.Vector'>
346
+
347
+ The bind function returns a new Vector object whose content is computed as the element-wise
348
+ multiplication of the two input vectors.
349
+ """
350
+
351
+ if not isinstance(vector, type(self)):
352
+ raise TypeError("Cannot apply the bind operator to non-Vector objects")
353
+
354
+ # Import arithmetic.bind here to avoid circular imports
355
+ from hdlib.arithmetic import bind as bind_operator
356
+
357
+ return bind_operator(self, vector)
358
+
359
+ def dist(self, vector: "Vector", method: str="cosine") -> float:
360
+ """Compute distance between vectors.
361
+
362
+ Parameters
363
+ ----------
364
+ vector : Vector
365
+ A Vector object from which the distance must be computed.
366
+ method : {'cosine', 'euclidean', 'hamming'}, optional, default 'cosine'
367
+ The distance method.
368
+
369
+ Returns
370
+ -------
371
+ float
372
+ The distance between the current Vector object and the input `vector`.
373
+
374
+ Raises
375
+ ------
376
+ Exception
377
+ If the current vector has a different size or vector type than the input vector.
378
+
379
+ Examples
380
+ --------
381
+ >>> from hdlib.vector import Vector
382
+ >>> vector1 = Vector(seed=1)
383
+ >>> vector2 = Vector(seed=2)
384
+ >>> vector1.dist(vector2, method='cosine')
385
+ 0.996
386
+
387
+ Generate two random bipolar vectors and compute the distance between them.
388
+ """
389
+
390
+ if self.size != vector.size:
391
+ raise Exception("Vectors must have the same size")
392
+
393
+ if self.vtype != vector.vtype:
394
+ raise Exception("Vectors must be of the same type")
395
+
396
+ if method.lower() == "cosine":
397
+ return 1 - np.dot(self.vector, vector.vector) / (np.linalg.norm(self.vector) * np.linalg.norm(vector.vector))
398
+
399
+ elif method.lower() == "hamming":
400
+ return np.count_nonzero(self.vector != vector.vector)
401
+
402
+ elif method.lower() == "euclidean":
403
+ return np.linalg.norm(self.vector - vector.vector)
404
+
405
+ else:
406
+ raise ValueError("Distance method \"{}\" is not supported".format(method))
407
+
408
+ def normalize(self) -> None:
409
+ """Normalize a vector after a binding or bundling with another vector.
410
+
411
+ Raises
412
+ ------
413
+ ValueError
414
+ If the vector type is not supported (i.e., is different from binary and bipolar).
415
+
416
+ Examples
417
+ --------
418
+ >>> from hdlib.vector import Vector
419
+ >>> from hdlib.arithmetic import bind
420
+ >>> vector1 = Vector()
421
+ >>> vector2 = Vector()
422
+ >>> vector3 = bind(vector1, vector2)
423
+ >>> vector3.normalize()
424
+ >>> ((vector3.vector == -1) | (vector3.vector == 1)).all()
425
+ True
426
+
427
+ Binding or bundling two vectors can produce a new vector whose vtype is different from the
428
+ one of the two input vector. This function normalizes the vector content in accordance to
429
+ its vector type.
430
+ """
431
+
432
+ if self.vtype == "bipolar":
433
+ # np.sign returns -1, 0, or 1
434
+ self.vector = np.sign(self.vector)
435
+
436
+ # Force 0 -> 1
437
+ self.vector[self.vector == 0] = 1
438
+
439
+ elif self.vtype == "binary":
440
+ # Find the scale (max value)
441
+ max_val = np.max(self.vector)
442
+
443
+ if max_val > 0:
444
+ # Normalize (element / max) and threshold at 0.5
445
+ # Equivalent to: element > max_val * 0.5
446
+ self.vector = np.where((self.vector / max_val) > 0.5, 1, 0)
447
+
448
+ else:
449
+ # If max is 0 (empty, null vector), ensure it stays 0
450
+ self.vector[:] = 0
451
+
452
+ else:
453
+ raise ValueError(f"Vector type '{self.vtype}' is not supported")
454
+
455
+ # Ensure the array remains integers
456
+ self.vector = self.vector.astype(int)
457
+
458
+ def bind(self, vector: "Vector") -> None:
459
+ """Bind the current vector with another vector object inplace.
460
+
461
+ Parameters
462
+ ----------
463
+ vector : Vector
464
+ The input Vector object.
465
+
466
+ Examples
467
+ --------
468
+ >>> from hdlib.vector import Vector
469
+ >>> vector1 = Vector()
470
+ >>> vector2 = Vector()
471
+ >>> vector1.bind(vector2)
472
+
473
+ It overrides the actual vector content of `vector1` with the result of the binding with `vector2`.
474
+ Refers to hdlib.arithmetic.bind for additional information.
475
+ """
476
+
477
+ # Import arithmetic.bind here to avoid circular imports
478
+ from hdlib.arithmetic import bind as bind_operator
479
+
480
+ self.__override_object(bind_operator(self, vector))
481
+
482
+ def bundle(self, vector: "Vector") -> None:
483
+ """Bundle the current vector with another vector object inplace.
484
+
485
+ Parameters
486
+ ----------
487
+ vector : Vector
488
+ The input Vector object.
489
+
490
+ Examples
491
+ --------
492
+ >>> from hdlib.vector import Vector
493
+ >>> vector1 = Vector()
494
+ >>> vector2 = Vector()
495
+ >>> vector1.bundle(vector2)
496
+
497
+ It overrides the actual vector content of `vector1` with the result of the bundling with `vector2`.
498
+ Refers to hdlib.arithmetic.bundle for additional information.
499
+ """
500
+
501
+ # Import arithmetic.bundle here to avoid circular imports
502
+ from hdlib.arithmetic import bundle as bundle_operator
503
+
504
+ self.__override_object(bundle_operator(self, vector))
505
+
506
+ def subtraction(self, vector: "Vector") -> None:
507
+ """Subtract a vector from the current vector object inplace.
508
+
509
+ Parameters
510
+ ----------
511
+ vector : Vector
512
+ The input Vector object.
513
+
514
+ Examples
515
+ --------
516
+ >>> from hdlib.vector import Vector
517
+ >>> vector1 = Vector()
518
+ >>> vector2 = Vector()
519
+ >>> vector1.subtract(vector2)
520
+
521
+ It overrides the actual vector content of `vector1` with the result of the subtraction with `vector2`.
522
+ Refers to hdlib.arithmetic.subtraction for additional information.
523
+ """
524
+
525
+ # Import arithmetic.subtraction here to avoid circular imports
526
+ from hdlib.arithmetic import subtraction as subtraction_operator
527
+
528
+ self.__override_object(subtraction_operator(self, vector))
529
+
530
+ def permute(self, rotate_by: int=1) -> None:
531
+ """Permute the current vector inplace.
532
+
533
+ Parameters
534
+ ----------
535
+ rotate_by : int
536
+ Rotate the input vector by `rotate_by` positions (the default is 1).
537
+
538
+ Examples
539
+ --------
540
+ >>> from hdlib.vector import Vector
541
+ >>> vector = Vector()
542
+ >>> vector.permute(rotate_by=2)
543
+
544
+ It overrides the actual vector content of `vector` with the result of applying the permute function inplace.
545
+ Refers to hdlib.arithmetic.permute for additional information.
546
+ """
547
+
548
+ # Import arithmetic.permute here to avoid circular imports
549
+ from hdlib.arithmetic import permute as permute_operator
550
+
551
+ self.__override_object(permute_operator(self, rotate_by=rotate_by))
552
+
553
+ def __override_object(self, vector: "Vector") -> None:
554
+ """Override the Vector object with another Vector object. This is a private method.
555
+
556
+ Parameters
557
+ ----------
558
+ vector : Vector
559
+ The input vector from which properties are inherited to the current vector.
560
+ """
561
+
562
+ self.name = vector.name
563
+ self.size = vector.size
564
+ self.seed = vector.seed
565
+ self.tags = vector.tags
566
+
567
+ self.parents = vector.parents
568
+ self.children = vector.children
569
+
570
+ self.vtype = vector.vtype
571
+ self.vector = vector.vector
572
+
573
+ self.version = vector.version
574
+
575
+ def dump(self, to_file: Optional[os.path.abspath]=None) -> None:
576
+ """Dump the Vector object to a pickle file.
577
+
578
+ Parameters
579
+ ----------
580
+ to_file
581
+ Path to the file used to dump the Vector object to.
582
+
583
+ Raises
584
+ ------
585
+ Exception
586
+ If the `to_file` file already exists.
587
+
588
+ Examples
589
+ --------
590
+ >>> import os
591
+ >>> from hdlib.vector import Vector
592
+ >>> vector = Vector()
593
+ >>> vector.dump(to_file='~/my_vector.pkl')
594
+ >>> os.path.isfile('~/my_vector.pkl')
595
+ True
596
+
597
+ Create a Vector object and dump it to a pickle file under the home directory.
598
+ """
599
+
600
+ if not to_file:
601
+ # Dump the vector to a pickle file in the current working directory if not file path is provided
602
+ to_file = os.path.join(os.getcwd(), "{}.pkl".format(self.name))
603
+
604
+ if os.path.isfile(to_file):
605
+ raise Exception("The output file already exists!\n{}".format(to_file))
606
+
607
+ with open(to_file, "wb") as pkl:
608
+ pickle.dump(self, pkl)