diff --git a/.codespellrc b/.codespellrc deleted file mode 100644 index 5aa4b5e..0000000 --- a/.codespellrc +++ /dev/null @@ -1,3 +0,0 @@ -[codespell] -skip = .git,*.pdf,*.svg -# ignore-words-list = diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..902b3b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# NOTE: run `pre-commit autoupdate` to update hooks to latest version +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files + - id: check-json + - id: check-toml + - id: name-tests-test + args: [--pytest-test-first] + - id: check-docstring-first +- repo: https://github.com/psf/black + rev: 23.12.0 + hooks: + - id: black +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 + hooks: + - id: ruff +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/README.md b/README.md index 52b22ea..410ca5b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,30 @@ Matlab: generateExtension('/ndx-events/spec/ndx-events.namespace.yaml'); ``` +## Developer installation +In a Python 3.8-3.12 environment: +```bash +pip install -r requirements-dev.txt +pip install -e . +``` + +Run tests: +```bash +pytest +``` + +Install pre-commit hooks: +```bash +pre-commit install +``` + +Style and other checks: +```bash +black . +ruff . +codespell . +``` + ## Example usage Python: diff --git a/src/pynwb/tests/test_example_usage.py b/src/pynwb/tests/test_example_usage.py index ba32e10..1518a19 100644 --- a/src/pynwb/tests/test_example_usage.py +++ b/src/pynwb/tests/test_example_usage.py @@ -1,4 +1,4 @@ -def test_example_usage(): +def test_example_usage1(): from datetime import datetime from ndx_events import EventsTable, EventTypesTable, TtlsTable, TtlTypesTable, Task import numpy as np @@ -42,7 +42,7 @@ def test_example_usage(): ) learning_response_description = ( "During the learning phase, subjects are instructed to respond to the following " - "question: 'Is this an animal?' in each trial. Response are encoded as 'Yes, this " + "question: 'Is this an animal?' in each trial. Responses are encoded as 'Yes, this " "is an animal' (20) and 'No, this is not an animal' (21)." ) ttl_types_table.add_row( @@ -185,5 +185,55 @@ def test_example_usage(): print(read_nwbfile.acquisition["TtlsTable"].to_dataframe()) +def test_example_usage2(): + """Example storing lick times""" + from datetime import datetime + from ndx_events import EventsTable, EventTypesTable, Task + import numpy as np + from pynwb import NWBFile, NWBHDF5IO + + nwbfile = NWBFile( + session_description="session description", + identifier="cool_experiment_001", + session_start_time=datetime.now().astimezone(), + ) + + # NOTE that when adding an EventTypesTable to a Task, the EventTypesTable + # must be named "event_types" according to the spec + event_types_table = EventTypesTable(name="event_types", description="Metadata about event types") + event_types_table.add_row( + event_name="lick", + event_type_description="Times when the subject licked the port", + ) + + # create a random sorted array of 1000 lick timestamps (dtype=float) from 0 to 3600 seconds + lick_times = sorted(np.random.uniform(0, 3600, 1000)) + + events_table = EventsTable(description="Metadata about events", target_tables={"event_type": event_types_table}) + for t in lick_times: + # event_type=0 corresponds to the first row in the event_types_table + events_table.add_row(timestamp=t, event_type=0) + events_table.timestamp.resolution = 1 / 30000.0 # licks were detected at 30 kHz + + task = Task() + task.event_types = event_types_table + nwbfile.add_lab_meta_data(task) + nwbfile.add_acquisition(events_table) + + # write nwb file + filename = "test.nwb" + with NWBHDF5IO(filename, "w") as io: + io.write(nwbfile) + + # read nwb file and check its contents + with NWBHDF5IO(filename, "r", load_namespaces=True) as io: + read_nwbfile = io.read() + print(read_nwbfile) + # access the events table and event types table and print them + print(read_nwbfile.get_lab_meta_data("task").event_types.to_dataframe()) + print(read_nwbfile.acquisition["EventsTable"].to_dataframe()) + + if __name__ == "__main__": - test_example_usage() + test_example_usage1() + test_example_usage2() diff --git a/src/spec/create_extension_spec.py b/src/spec/create_extension_spec.py index 75d47f9..32a5f60 100644 --- a/src/spec/create_extension_spec.py +++ b/src/spec/create_extension_spec.py @@ -17,7 +17,7 @@ def main(): timestamp_vector_data = NWBDatasetSpec( neurodata_type_def="TimestampVectorData", neurodata_type_inc="VectorData", - doc="A VectorData that stores timestamps in seconds.", + doc="A 1-dimensional VectorData that stores timestamps in seconds.", dtype="float64", dims=["num_times"], shape=[None], @@ -26,8 +26,11 @@ def main(): name="unit", dtype="text", doc="The unit of measurement for the timestamps, fixed to 'seconds'.", - value="seconds", + value="xseconds", ), + # NOTE: this requires all timestamps to have the same resolution which may not be true + # if they come from different acquisition systems or processing pipelines... + # maybe this should be a column of the event type table instead? NWBAttributeSpec( name="resolution", dtype="float64", @@ -43,7 +46,7 @@ def main(): duration_vector_data = NWBDatasetSpec( neurodata_type_def="DurationVectorData", neurodata_type_inc="VectorData", - doc="A VectorData that stores durations in seconds.", + doc="A 1-dimensional VectorData that stores durations in seconds.", dtype="float64", dims=["num_events"], shape=[None], @@ -54,6 +57,7 @@ def main(): doc="The unit of measurement for the durations, fixed to 'seconds'.", value="seconds", ), + # NOTE: this is usually the same as the timestamp resolution NWBAttributeSpec( name="resolution", dtype="float64", @@ -92,10 +96,11 @@ def main(): neurodata_type_inc="DynamicTable", doc=( "A column-based table to store information about events (event instances), one event per row. " - "Each event must have an event_type, which is a row in the EventTypesTable. Additional columns " - "may be added to store metadata about each event, such as the duration of the event, or a " - "text value of the event." + "Each event must have an event_type, which is a reference to a row in the EventTypesTable. " + "Additional columns may be added to store metadata about each event, such as the duration " + "of the event, or a text value of the event." ), + # NOTE: custom columns should apply to every event in the table which may not be the case default_name="EventsTable", datasets=[ NWBDatasetSpec(