harp-python 0.1.0__tar.gz → 0.2.0__tar.gz

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 (32) hide show
  1. {harp-python-0.1.0 → harp-python-0.2.0}/.gitignore +1 -1
  2. {harp-python-0.1.0 → harp-python-0.2.0}/PKG-INFO +33 -16
  3. harp-python-0.2.0/README.md +58 -0
  4. harp-python-0.2.0/harp/__init__.py +3 -0
  5. harp-python-0.2.0/harp/io.py +129 -0
  6. harp-python-0.2.0/harp/model.py +202 -0
  7. harp-python-0.2.0/harp/reader.py +257 -0
  8. harp-python-0.2.0/harp/schema.py +53 -0
  9. {harp-python-0.1.0 → harp-python-0.2.0}/harp_python.egg-info/PKG-INFO +33 -16
  10. {harp-python-0.1.0 → harp-python-0.2.0}/harp_python.egg-info/SOURCES.txt +10 -1
  11. {harp-python-0.1.0 → harp-python-0.2.0}/harp_python.egg-info/requires.txt +1 -0
  12. {harp-python-0.1.0 → harp-python-0.2.0}/pyproject.toml +1 -1
  13. harp-python-0.2.0/tests/__init__.py +0 -0
  14. harp-python-0.2.0/tests/data/device.yml +35 -0
  15. harp-python-0.2.0/tests/data/device_0.bin +0 -0
  16. harp-python-0.2.0/tests/data/write_0.bin +0 -0
  17. harp-python-0.2.0/tests/params.py +41 -0
  18. harp-python-0.2.0/tests/test_io.py +55 -0
  19. harp-python-0.2.0/tests/test_reader.py +32 -0
  20. harp-python-0.2.0/tests/test_schema.py +17 -0
  21. harp-python-0.1.0/README.md +0 -42
  22. harp-python-0.1.0/harp/io.py +0 -58
  23. harp-python-0.1.0/harp/model.py +0 -184
  24. harp-python-0.1.0/harp/reader.py +0 -155
  25. harp-python-0.1.0/harp/schema.py +0 -31
  26. {harp-python-0.1.0 → harp-python-0.2.0}/.gitmodules +0 -0
  27. {harp-python-0.1.0 → harp-python-0.2.0}/LICENSE +0 -0
  28. {harp-python-0.1.0 → harp-python-0.2.0}/MANIFEST.in +0 -0
  29. {harp-python-0.1.0 → harp-python-0.2.0}/harp/common.yml +0 -0
  30. {harp-python-0.1.0 → harp-python-0.2.0}/harp_python.egg-info/dependency_links.txt +0 -0
  31. {harp-python-0.1.0 → harp-python-0.2.0}/harp_python.egg-info/top_level.txt +0 -0
  32. {harp-python-0.1.0 → harp-python-0.2.0}/setup.cfg +0 -0
@@ -9,4 +9,4 @@ __pycache__
9
9
  dist
10
10
 
11
11
  # Data files
12
- data
12
+ /data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: harp-python
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A low-level interface for loading binary Harp protocol data
5
5
  Author-email: Goncalo Lopes <g.lopes@neurogears.org>
6
6
  License: MIT License
@@ -20,6 +20,7 @@ Requires-Dist: pydantic-yaml
20
20
  Requires-Dist: pandas
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: datamodel-code-generator; extra == "dev"
23
+ Requires-Dist: pytest; extra == "dev"
23
24
  Requires-Dist: black; extra == "dev"
24
25
  Provides-Extra: jupyter
25
26
  Requires-Dist: ipykernel; extra == "jupyter"
@@ -31,35 +32,33 @@ A low-level interface to data collected with the [Harp](https://harp-tech.org/)
31
32
 
32
33
  ## Data model
33
34
 
34
- To regenerate Pydantic data models from device schema definitions, activate a virtual environment with `dev` dependencies, and run:
35
+ The interface makes use of a Pydantic data model generated from Harp device schema definitions. The schema data classes are used to automatically generate binary readers for each device.
36
+
37
+ All binary data files from a single device need to be stored in the same folder alongside the device meta-schema, named `device.yml`. Each register file should have the following naming convention `<deviceName>_<registerAddress>.bin`.
38
+
39
+ For example, for a dataset collected with a `Behavior` device, you might have:
35
40
 
36
41
  ```
37
- datamodel-codegen --input ./reflex-generator/schema/device.json --output harp/model.py --output-model-type pydantic_v2.BaseModel
42
+ 📦device.harp
43
+ ┣ 📜Behavior_0.bin
44
+ ┣ 📜Behavior_1.bin
45
+ ...
46
+ ┗ 📜device.yml
38
47
  ```
39
48
 
40
- > [!IMPORTANT]
41
- > Currently code generation adds an unwanted field at the very end of the data model definition `registers: Optional[Any] = None`. This declaration needs to be removed for serialization to work properly.
42
-
43
49
  ## How to use
44
50
 
45
- ### Read Harp device schema from YML file
46
-
47
- ```python
48
- from harp.schema import read_schema
49
- schema = read_schema('device.yml')
50
- ```
51
-
52
51
  ### Create device reader object from schema
53
52
 
54
53
  ```python
55
- from harp.reader import create_reader
56
- reader = create_reader(schema)
54
+ import harp
55
+ reader = harp.create_reader("device.harp")
57
56
  ```
58
57
 
59
58
  ### Read data from named register
60
59
 
61
60
  ```python
62
- reader.OperationControl.read("data/Behavior_10.bin")
61
+ reader.OperationControl.read()
63
62
  ```
64
63
 
65
64
  ### Access register metadata
@@ -67,3 +66,21 @@ reader.OperationControl.read("data/Behavior_10.bin")
67
66
  ```python
68
67
  reader.OperationControl.register.address
69
68
  ```
69
+
70
+ ### Create device reader object with UTC datetime format
71
+
72
+ ```python
73
+ reader = harp.create_reader("device.harp", epoch=harp.REFERENCE_EPOCH)
74
+ ```
75
+
76
+ ### Read data with message type information
77
+
78
+ ```python
79
+ reader.OperationControl.read(keep_type=True)
80
+ ```
81
+
82
+ ### Read data from a specific file
83
+
84
+ ```python
85
+ reader.OperationControl.read("data/Behavior_10.bin")
86
+ ```
@@ -0,0 +1,58 @@
1
+ # harp-python
2
+
3
+ A low-level interface to data collected with the [Harp](https://harp-tech.org/) binary protocol.
4
+
5
+ ## Data model
6
+
7
+ The interface makes use of a Pydantic data model generated from Harp device schema definitions. The schema data classes are used to automatically generate binary readers for each device.
8
+
9
+ All binary data files from a single device need to be stored in the same folder alongside the device meta-schema, named `device.yml`. Each register file should have the following naming convention `<deviceName>_<registerAddress>.bin`.
10
+
11
+ For example, for a dataset collected with a `Behavior` device, you might have:
12
+
13
+ ```
14
+ 📦device.harp
15
+ ┣ 📜Behavior_0.bin
16
+ ┣ 📜Behavior_1.bin
17
+ ...
18
+ ┗ 📜device.yml
19
+ ```
20
+
21
+ ## How to use
22
+
23
+ ### Create device reader object from schema
24
+
25
+ ```python
26
+ import harp
27
+ reader = harp.create_reader("device.harp")
28
+ ```
29
+
30
+ ### Read data from named register
31
+
32
+ ```python
33
+ reader.OperationControl.read()
34
+ ```
35
+
36
+ ### Access register metadata
37
+
38
+ ```python
39
+ reader.OperationControl.register.address
40
+ ```
41
+
42
+ ### Create device reader object with UTC datetime format
43
+
44
+ ```python
45
+ reader = harp.create_reader("device.harp", epoch=harp.REFERENCE_EPOCH)
46
+ ```
47
+
48
+ ### Read data with message type information
49
+
50
+ ```python
51
+ reader.OperationControl.read(keep_type=True)
52
+ ```
53
+
54
+ ### Read data from a specific file
55
+
56
+ ```python
57
+ reader.OperationControl.read("data/Behavior_10.bin")
58
+ ```
@@ -0,0 +1,3 @@
1
+ from harp.io import REFERENCE_EPOCH, MessageType, read
2
+ from harp.reader import create_reader
3
+ from harp.schema import read_schema
@@ -0,0 +1,129 @@
1
+ from datetime import datetime
2
+ from enum import IntEnum
3
+ from os import PathLike
4
+ from typing import Any, BinaryIO, Optional, Union
5
+ from pandas._typing import Axes
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ REFERENCE_EPOCH = datetime(1904, 1, 1)
10
+ """The reference epoch for UTC harp time."""
11
+
12
+
13
+ class MessageType(IntEnum):
14
+ """Specifies the type of a Harp message."""
15
+
16
+ NA = 0
17
+ READ = 1
18
+ WRITE = 2
19
+ EVENT = 3
20
+
21
+
22
+ _SECONDS_PER_TICK = 32e-6
23
+ _messagetypes = [type.name for type in MessageType]
24
+ _payloadtypes = {
25
+ 1: np.dtype(np.uint8),
26
+ 2: np.dtype(np.uint16),
27
+ 4: np.dtype(np.uint32),
28
+ 8: np.dtype(np.uint64),
29
+ 129: np.dtype(np.int8),
30
+ 130: np.dtype(np.int16),
31
+ 132: np.dtype(np.int32),
32
+ 136: np.dtype(np.int64),
33
+ 68: np.dtype(np.float32),
34
+ }
35
+
36
+
37
+ def read(
38
+ file: Union[str, bytes, PathLike[Any], BinaryIO],
39
+ address: Optional[int] = None,
40
+ dtype: Optional[np.dtype] = None,
41
+ length: Optional[int] = None,
42
+ columns: Optional[Axes] = None,
43
+ epoch: Optional[datetime] = None,
44
+ keep_type: bool = False,
45
+ ):
46
+ """Read single-register Harp data from the specified file.
47
+
48
+ Parameters
49
+ ----------
50
+ file
51
+ Open file object or filename containing binary data from
52
+ a single device register.
53
+ address
54
+ Expected register address. If specified, the address of
55
+ the first message in the file is used for validation.
56
+ dtype
57
+ Expected data type of the register payload. If specified, the
58
+ payload type of the first message in the file is used for validation.
59
+ length
60
+ Expected number of elements in register payload. If specified, the
61
+ payload length of the first message in the file is used for validation.
62
+ columns
63
+ The optional column labels to use for the data values.
64
+ epoch
65
+ Reference datetime at which time zero begins. If specified,
66
+ the result data frame will have a datetime index.
67
+ keep_type
68
+ Specifies whether to include a column with the message type.
69
+
70
+ Returns
71
+ -------
72
+ A pandas data frame containing message data, sorted by time.
73
+ """
74
+ data = np.fromfile(file, dtype=np.uint8)
75
+ if len(data) == 0:
76
+ return pd.DataFrame(
77
+ columns=columns, index=pd.Index([], dtype=np.float64, name="Time")
78
+ )
79
+
80
+ if address is not None and address != data[2]:
81
+ raise ValueError(f"expected address {address} but got {data[2]}")
82
+
83
+ index = None
84
+ stride = data[1] + 2
85
+ nrows = len(data) // stride
86
+ payloadtype = data[4]
87
+ payloadoffset = 5
88
+ if payloadtype & 0x10 != 0:
89
+ seconds = np.ndarray(
90
+ nrows, dtype=np.uint32, buffer=data, offset=payloadoffset, strides=stride
91
+ )
92
+ payloadoffset += 4
93
+ micros = np.ndarray(
94
+ nrows, dtype=np.uint16, buffer=data, offset=payloadoffset, strides=stride
95
+ )
96
+ payloadoffset += 2
97
+ time = micros * _SECONDS_PER_TICK + seconds
98
+ payloadtype = payloadtype & ~0x10
99
+ if epoch is not None:
100
+ time = epoch + pd.to_timedelta(time, "s") # type: ignore
101
+ index = pd.Series(time)
102
+ index.name = "Time"
103
+
104
+ payloadsize = stride - payloadoffset - 1
105
+ payloadtype = _payloadtypes[payloadtype]
106
+ if dtype is not None and dtype != payloadtype:
107
+ raise ValueError(f"expected payload type {dtype} but got {payloadtype}")
108
+
109
+ elementsize = payloadtype.itemsize
110
+ payloadshape = (nrows, payloadsize // elementsize)
111
+ if length is not None and length != payloadshape[1]:
112
+ raise ValueError(f"expected payload length {length} but got {payloadshape[1]}")
113
+
114
+ payload = np.ndarray(
115
+ payloadshape,
116
+ dtype=payloadtype,
117
+ buffer=data,
118
+ offset=payloadoffset,
119
+ strides=(stride, elementsize),
120
+ )
121
+
122
+ result = pd.DataFrame(payload, index=index, columns=columns)
123
+ if keep_type:
124
+ msgtype = np.ndarray(
125
+ nrows, dtype=np.uint8, buffer=data, offset=0, strides=stride
126
+ )
127
+ msgtype = pd.Categorical.from_codes(msgtype, categories=_messagetypes) # type: ignore
128
+ result[MessageType.__name__] = msgtype
129
+ return result
@@ -0,0 +1,202 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: device.json
3
+ # timestamp: 2023-10-30T11:46:57+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Dict, List, Optional, Union
9
+ from typing_extensions import Annotated
10
+
11
+ from pydantic import (
12
+ BaseModel,
13
+ BeforeValidator,
14
+ ConfigDict,
15
+ Field,
16
+ RootModel,
17
+ field_serializer,
18
+ )
19
+
20
+
21
+ class PayloadType(str, Enum):
22
+ U8 = "uint8"
23
+ S8 = "int8"
24
+ U16 = "uint16"
25
+ S16 = "int16"
26
+ U32 = "uint32"
27
+ S32 = "int32"
28
+ U64 = "uint64"
29
+ S64 = "int64"
30
+ Float = "float32"
31
+
32
+
33
+ class Access(Enum):
34
+ Read = "Read"
35
+ Write = "Write"
36
+ Event = "Event"
37
+
38
+
39
+ class MaskValueItem(BaseModel):
40
+ model_config = ConfigDict(
41
+ extra="forbid",
42
+ )
43
+ value: int = Field(..., description="Specifies the numerical mask value.")
44
+ description: Optional[str] = Field(
45
+ None, description="Specifies a summary description of the mask value function."
46
+ )
47
+
48
+ def __int__(self):
49
+ return self.value
50
+
51
+
52
+ class MaskValue(RootModel[Union[int, MaskValueItem]]):
53
+ root: Union[int, MaskValueItem]
54
+
55
+
56
+ class BitMask(BaseModel):
57
+ description: Optional[str] = Field(
58
+ None, description="Specifies a summary description of the bit mask function."
59
+ )
60
+ bits: Dict[str, MaskValue]
61
+
62
+
63
+ class GroupMask(BaseModel):
64
+ description: Optional[str] = Field(
65
+ None, description="Specifies a summary description of the group mask function."
66
+ )
67
+ values: Dict[str, MaskValue]
68
+
69
+
70
+ class MaskType(RootModel[str]):
71
+ root: str = Field(
72
+ ...,
73
+ description="Specifies the name of the bit mask or group mask used to represent the payload value.",
74
+ )
75
+
76
+
77
+ class InterfaceType(RootModel[str]):
78
+ root: str = Field(
79
+ ...,
80
+ description="Specifies the name of the type used to represent the payload value in the high-level interface.",
81
+ )
82
+
83
+
84
+ class Converter(Enum):
85
+ None_ = "None"
86
+ Payload = "Payload"
87
+ RawPayload = "RawPayload"
88
+
89
+
90
+ class MinValue(RootModel[float]):
91
+ root: float = Field(
92
+ ...,
93
+ description="Specifies the minimum allowable value for the payload or payload member.",
94
+ )
95
+
96
+
97
+ class MaxValue(RootModel[float]):
98
+ root: float = Field(
99
+ ...,
100
+ description="Specifies the maximum allowable value for the payload or payload member.",
101
+ )
102
+
103
+
104
+ class DefaultValue(RootModel[float]):
105
+ root: float = Field(
106
+ ...,
107
+ description="Specifies the default value for the payload or payload member.",
108
+ )
109
+
110
+
111
+ class PayloadMember(BaseModel):
112
+ mask: Optional[int] = Field(
113
+ None,
114
+ description="Specifies the mask used to read and write this payload member.",
115
+ )
116
+ offset: Optional[int] = Field(
117
+ None,
118
+ description="Specifies the payload array offset where this payload member is stored.",
119
+ )
120
+ description: Optional[str] = Field(
121
+ None, description="Specifies a summary description of the payload member."
122
+ )
123
+ minValue: Optional[MinValue] = None
124
+ maxValue: Optional[MaxValue] = None
125
+ defaultValue: Optional[DefaultValue] = None
126
+ maskType: Optional[MaskType] = None
127
+ interfaceType: Optional[InterfaceType] = None
128
+ converter: Optional[Converter] = None
129
+
130
+
131
+ class Visibility(Enum):
132
+ public = "public"
133
+ private = "private"
134
+
135
+
136
+ class Register(BaseModel):
137
+ address: Annotated[
138
+ int,
139
+ Field(
140
+ le=255, description="Specifies the unique 8-bit address of the register."
141
+ ),
142
+ ]
143
+ type: Annotated[PayloadType, BeforeValidator(lambda v: PayloadType[v])]
144
+ length: Annotated[
145
+ Optional[int],
146
+ Field(
147
+ ge=1, default=1, description="Specifies the length of the register payload."
148
+ ),
149
+ ]
150
+ access: Union[Access, List[Access]] = Field(
151
+ ..., description="Specifies the expected use of the register."
152
+ )
153
+ description: Optional[str] = Field(
154
+ None, description="Specifies a summary description of the register function."
155
+ )
156
+ minValue: Optional[MinValue] = None
157
+ maxValue: Optional[MaxValue] = None
158
+ defaultValue: Optional[DefaultValue] = None
159
+ maskType: Optional[MaskType] = None
160
+ visibility: Optional[Visibility] = Field(
161
+ None,
162
+ description="Specifies whether the register function is exposed in the high-level interface.",
163
+ )
164
+ volatile: Optional[bool] = Field(
165
+ None,
166
+ description="Specifies whether register values can be saved in non-volatile memory.",
167
+ )
168
+ payloadSpec: Optional[Dict[str, PayloadMember]] = None
169
+ interfaceType: Optional[InterfaceType] = None
170
+ converter: Optional[Converter] = None
171
+
172
+ @field_serializer("type")
173
+ def _serialize_type(self, type: PayloadType):
174
+ return type.name
175
+
176
+
177
+ class Registers(BaseModel):
178
+ registers: Dict[str, Register] = Field(
179
+ ...,
180
+ description="Specifies the collection of registers implementing the device function.",
181
+ )
182
+ bitMasks: Optional[Dict[str, BitMask]] = Field(
183
+ None,
184
+ description="Specifies the collection of masks available to be used with the different registers.",
185
+ )
186
+ groupMasks: Optional[Dict[str, GroupMask]] = Field(
187
+ None,
188
+ description="Specifies the collection of group masks available to be used with the different registers.",
189
+ )
190
+
191
+
192
+ class Model(Registers):
193
+ device: str = Field(..., description="Specifies the name of the device.")
194
+ whoAmI: int = Field(
195
+ ..., description="Specifies the unique identifier for this device type."
196
+ )
197
+ firmwareVersion: str = Field(
198
+ ..., description="Specifies the semantic version of the device firmware."
199
+ )
200
+ hardwareTargets: str = Field(
201
+ ..., description="Specifies the semantic version of the device hardware."
202
+ )