mirror of
https://github.com/p2p-ld/nwb-linkml.git
synced 2025-01-10 06:04:28 +00:00
more docs for dataset adapters.
also: - fix search for initial - in dataset doctest - don't clear git repos on repeated test runs - fix string handling in load_yaml
This commit is contained in:
parent
ce902476d1
commit
f4d397cde1
5 changed files with 283 additions and 10 deletions
|
@ -19,7 +19,7 @@ NWB_KEYS = re.compile(r"(^\s*datasets:\s*\n)|^groups:")
|
||||||
def _strip_nwb(nwb: str) -> str:
|
def _strip_nwb(nwb: str) -> str:
|
||||||
# strip 'datasets:' keys and decoration left in for readability/context
|
# strip 'datasets:' keys and decoration left in for readability/context
|
||||||
nwb = re.sub(NWB_KEYS, "", nwb)
|
nwb = re.sub(NWB_KEYS, "", nwb)
|
||||||
nwb = re.sub(r"-", " ", nwb)
|
nwb = re.sub(r"^-", " ", nwb)
|
||||||
nwb = textwrap.dedent(nwb)
|
nwb = textwrap.dedent(nwb)
|
||||||
return nwb
|
return nwb
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ArrayAdapter:
|
||||||
Dict[Literal["any_of"], Dict[Literal["array"], List[ArrayExpression]]],
|
Dict[Literal["any_of"], Dict[Literal["array"], List[ArrayExpression]]],
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
Make the array expressions in a dict form that can be **kwarg'd into a SlotDefinition,
|
Make the array expressions in a dict form that can be ``**kwarg``'d into a SlotDefinition,
|
||||||
taking into account needing to use ``any_of`` for multiple array range specifications.
|
taking into account needing to use ``any_of`` for multiple array range specifications.
|
||||||
"""
|
"""
|
||||||
expressions = self.make()
|
expressions = self.make()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Adapter for NWB datasets to linkml Classes
|
Adapter for NWB datasets to linkml Classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import ClassVar, Optional, Type
|
from typing import ClassVar, Optional, Type
|
||||||
|
|
||||||
|
@ -119,6 +118,54 @@ class MapScalarAttributes(DatasetMap):
|
||||||
"""
|
"""
|
||||||
A scalar with attributes gets an additional slot "value" that contains the actual scalar
|
A scalar with attributes gets an additional slot "value" that contains the actual scalar
|
||||||
value of this field
|
value of this field
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. adapter:: DatasetAdapter
|
||||||
|
:nwb:
|
||||||
|
datasets:
|
||||||
|
- name: starting_time
|
||||||
|
dtype: float64
|
||||||
|
doc: Timestamp of the first sample in seconds. When timestamps are uniformly
|
||||||
|
spaced, the timestamp of the first sample can be specified and all subsequent
|
||||||
|
ones calculated from the sampling rate attribute.
|
||||||
|
quantity: '?'
|
||||||
|
attributes:
|
||||||
|
- name: rate
|
||||||
|
dtype: float32
|
||||||
|
doc: Sampling rate, in Hz.
|
||||||
|
- name: unit
|
||||||
|
dtype: text
|
||||||
|
value: seconds
|
||||||
|
doc: Unit of measurement for time, which is fixed to 'seconds'.
|
||||||
|
:linkml:
|
||||||
|
classes:
|
||||||
|
- name: starting_time
|
||||||
|
description: Timestamp of the first sample in seconds. When timestamps are uniformly
|
||||||
|
spaced, the timestamp of the first sample can be specified and all subsequent
|
||||||
|
ones calculated from the sampling rate attribute.
|
||||||
|
attributes:
|
||||||
|
name:
|
||||||
|
name: name
|
||||||
|
ifabsent: string(starting_time)
|
||||||
|
identifier: true
|
||||||
|
range: string
|
||||||
|
required: true
|
||||||
|
equals_string: starting_time
|
||||||
|
rate:
|
||||||
|
name: rate
|
||||||
|
description: Sampling rate, in Hz.
|
||||||
|
range: float32
|
||||||
|
unit:
|
||||||
|
name: unit
|
||||||
|
description: Unit of measurement for time, which is fixed to 'seconds'.
|
||||||
|
range: text
|
||||||
|
value:
|
||||||
|
name: value
|
||||||
|
range: float64
|
||||||
|
required: true
|
||||||
|
tree_root: true
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -133,7 +180,7 @@ class MapScalarAttributes(DatasetMap):
|
||||||
* - ``neurodata_type_inc``
|
* - ``neurodata_type_inc``
|
||||||
- ``None``
|
- ``None``
|
||||||
* - ``attributes``
|
* - ``attributes``
|
||||||
- Truthy
|
- ``True``
|
||||||
* - ``dims``
|
* - ``dims``
|
||||||
- ``None``
|
- ``None``
|
||||||
* - ``shape``
|
* - ``shape``
|
||||||
|
@ -167,13 +214,65 @@ class MapScalarAttributes(DatasetMap):
|
||||||
|
|
||||||
class MapListlike(DatasetMap):
|
class MapListlike(DatasetMap):
|
||||||
"""
|
"""
|
||||||
Datasets that refer to other datasets (that handle their own arrays)
|
Datasets that refer to a list of other datasets.
|
||||||
|
|
||||||
|
Used exactly once in the core schema, in ``ImageReferences`` -
|
||||||
|
an array of references to other ``Image`` datasets. We ignore the
|
||||||
|
usual array structure and unnest the implicit array into a slot names from the
|
||||||
|
target type rather than the oddly-named ``num_images`` dimension so that
|
||||||
|
ultimately in the pydantic model we get a nicely behaved single-level list.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. adapter:: DatasetAdapter
|
||||||
|
:nwb:
|
||||||
|
datasets:
|
||||||
|
- neurodata_type_def: ImageReferences
|
||||||
|
neurodata_type_inc: NWBData
|
||||||
|
dtype:
|
||||||
|
target_type: Image
|
||||||
|
reftype: object
|
||||||
|
dims:
|
||||||
|
- num_images
|
||||||
|
shape:
|
||||||
|
- null
|
||||||
|
doc: Ordered dataset of references to Image objects.
|
||||||
|
:linkml:
|
||||||
|
classes:
|
||||||
|
- name: ImageReferences
|
||||||
|
description: Ordered dataset of references to Image objects.
|
||||||
|
is_a: NWBData
|
||||||
|
attributes:
|
||||||
|
name:
|
||||||
|
name: name
|
||||||
|
identifier: true
|
||||||
|
range: string
|
||||||
|
required: true
|
||||||
|
image:
|
||||||
|
name: image
|
||||||
|
description: Ordered dataset of references to Image objects.
|
||||||
|
multivalued: true
|
||||||
|
range: Image
|
||||||
|
required: true
|
||||||
|
tree_root: true
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(c, cls: Dataset) -> bool:
|
def check(c, cls: Dataset) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if we are a 1D dataset that isn't a normal datatype
|
Check if we are a 1D dataset that isn't a normal datatype
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
* - Attr
|
||||||
|
- Value
|
||||||
|
* - :func:`.is_1d`
|
||||||
|
- ``True``
|
||||||
|
* - ``dtype``
|
||||||
|
- ``Class``
|
||||||
"""
|
"""
|
||||||
dtype = ClassAdapter.handle_dtype(cls.dtype)
|
dtype = ClassAdapter.handle_dtype(cls.dtype)
|
||||||
return is_1d(cls) and dtype != "AnyType" and dtype not in flat_to_linkml
|
return is_1d(cls) and dtype != "AnyType" and dtype not in flat_to_linkml
|
||||||
|
@ -202,13 +301,83 @@ class MapArraylike(DatasetMap):
|
||||||
Datasets without any additional attributes don't create their own subclass,
|
Datasets without any additional attributes don't create their own subclass,
|
||||||
they're just an array :).
|
they're just an array :).
|
||||||
|
|
||||||
Replace the base class with the array class, and make a slot that refers to it.
|
Replace the base class with a slot that defines the array.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
eg. from ``image.ImageSeries`` :
|
||||||
|
|
||||||
|
.. adapter:: DatasetAdapter
|
||||||
|
:nwb:
|
||||||
|
datasets:
|
||||||
|
- name: data
|
||||||
|
dtype: numeric
|
||||||
|
dims:
|
||||||
|
- - frame
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
- - frame
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
- z
|
||||||
|
shape:
|
||||||
|
- - null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- - null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
- null
|
||||||
|
doc: Binary data representing images across frames. If data are stored in an external
|
||||||
|
file, this should be an empty 3D array.
|
||||||
|
:linkml:
|
||||||
|
slots:
|
||||||
|
- name: data
|
||||||
|
description: Binary data representing images across frames. If data are stored in
|
||||||
|
an external file, this should be an empty 3D array.
|
||||||
|
multivalued: false
|
||||||
|
range: numeric
|
||||||
|
required: true
|
||||||
|
any_of:
|
||||||
|
- array:
|
||||||
|
dimensions:
|
||||||
|
- alias: frame
|
||||||
|
- alias: x
|
||||||
|
- alias: y
|
||||||
|
- array:
|
||||||
|
dimensions:
|
||||||
|
- alias: frame
|
||||||
|
- alias: x
|
||||||
|
- alias: y
|
||||||
|
- alias: z
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(c, cls: Dataset) -> bool:
|
def check(c, cls: Dataset) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if we're a plain array
|
Check if we're a plain array
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
* - Attr
|
||||||
|
- Value
|
||||||
|
* - ``name``
|
||||||
|
- ``True``
|
||||||
|
* - ``dims``
|
||||||
|
- ``True``
|
||||||
|
* - ``shape``
|
||||||
|
- ``True``
|
||||||
|
* - :func:`.has_attrs`
|
||||||
|
- ``False``
|
||||||
|
* - :func:`.is_compound`
|
||||||
|
- ``False``
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
cls.name and all([cls.dims, cls.shape]) and not has_attrs(cls) and not is_compound(cls)
|
cls.name and all([cls.dims, cls.shape]) and not has_attrs(cls) and not is_compound(cls)
|
||||||
|
@ -243,6 +412,88 @@ class MapArrayLikeAttributes(DatasetMap):
|
||||||
"""
|
"""
|
||||||
The most general case - treat everything that isn't handled by one of the special cases
|
The most general case - treat everything that isn't handled by one of the special cases
|
||||||
as an array!
|
as an array!
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. adapter:: DatasetAdapter
|
||||||
|
:nwb:
|
||||||
|
datasets:
|
||||||
|
- neurodata_type_def: Image
|
||||||
|
neurodata_type_inc: NWBData
|
||||||
|
dtype: numeric
|
||||||
|
dims:
|
||||||
|
- - x
|
||||||
|
- y
|
||||||
|
- - x
|
||||||
|
- y
|
||||||
|
- r, g, b
|
||||||
|
- - x
|
||||||
|
- y
|
||||||
|
- r, g, b, a
|
||||||
|
shape:
|
||||||
|
- - null
|
||||||
|
- null
|
||||||
|
- - null
|
||||||
|
- null
|
||||||
|
- 3
|
||||||
|
- - null
|
||||||
|
- null
|
||||||
|
- 4
|
||||||
|
doc: An abstract data type for an image. Shape can be 2-D (x, y), or 3-D where the
|
||||||
|
third dimension can have three or four elements, e.g. (x, y, (r, g, b)) or
|
||||||
|
(x, y, (r, g, b, a)).
|
||||||
|
attributes:
|
||||||
|
- name: resolution
|
||||||
|
dtype: float32
|
||||||
|
doc: Pixel resolution of the image, in pixels per centimeter.
|
||||||
|
required: false
|
||||||
|
- name: description
|
||||||
|
dtype: text
|
||||||
|
doc: Description of the image.
|
||||||
|
required: false
|
||||||
|
:linkml:
|
||||||
|
classes:
|
||||||
|
- name: Image
|
||||||
|
description: An abstract data type for an image. Shape can be 2-D (x, y), or 3-D
|
||||||
|
where the third dimension can have three or four elements, e.g. (x, y, (r, g,
|
||||||
|
b)) or (x, y, (r, g, b, a)).
|
||||||
|
is_a: NWBData
|
||||||
|
attributes:
|
||||||
|
name:
|
||||||
|
name: name
|
||||||
|
identifier: true
|
||||||
|
range: string
|
||||||
|
required: true
|
||||||
|
resolution:
|
||||||
|
name: resolution
|
||||||
|
description: Pixel resolution of the image, in pixels per centimeter.
|
||||||
|
range: float32
|
||||||
|
description:
|
||||||
|
name: description
|
||||||
|
description: Description of the image.
|
||||||
|
range: text
|
||||||
|
array:
|
||||||
|
name: array
|
||||||
|
range: numeric
|
||||||
|
any_of:
|
||||||
|
- array:
|
||||||
|
dimensions:
|
||||||
|
- alias: x
|
||||||
|
- alias: y
|
||||||
|
- array:
|
||||||
|
dimensions:
|
||||||
|
- alias: x
|
||||||
|
- alias: y
|
||||||
|
- alias: r_g_b
|
||||||
|
exact_cardinality: 3
|
||||||
|
- array:
|
||||||
|
dimensions:
|
||||||
|
- alias: x
|
||||||
|
- alias: y
|
||||||
|
- alias: r_g_b_a
|
||||||
|
exact_cardinality: 4
|
||||||
|
tree_root: true
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NEEDS_NAME = True
|
NEEDS_NAME = True
|
||||||
|
@ -487,7 +738,11 @@ class DatasetAdapter(ClassAdapter):
|
||||||
|
|
||||||
def is_1d(cls: Dataset) -> bool:
|
def is_1d(cls: Dataset) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the values of a dataset are 1-dimensional
|
Check if the values of a dataset are 1-dimensional.
|
||||||
|
|
||||||
|
Specifically:
|
||||||
|
* a single-layer dim/shape list of length 1, or
|
||||||
|
* a nested dim/shape list where every nested spec is of length 1
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
not any([isinstance(dim, list) for dim in cls.dims]) and len(cls.dims) == 1
|
not any([isinstance(dim, list) for dim in cls.dims]) and len(cls.dims) == 1
|
||||||
|
|
|
@ -20,7 +20,16 @@ def load_yaml(path: Path | str) -> dict:
|
||||||
"""
|
"""
|
||||||
Load yaml file from file, applying postload modifications
|
Load yaml file from file, applying postload modifications
|
||||||
"""
|
"""
|
||||||
if isinstance(path, str) and not Path(path).exists():
|
is_file = False
|
||||||
|
try:
|
||||||
|
a_path = Path(path)
|
||||||
|
if a_path.exists():
|
||||||
|
is_file = True
|
||||||
|
except OSError:
|
||||||
|
# long strings can't be made into paths!
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not is_file:
|
||||||
ns_dict = yaml.safe_load(path)
|
ns_dict = yaml.safe_load(path)
|
||||||
else:
|
else:
|
||||||
with open(path) as file:
|
with open(path) as file:
|
||||||
|
|
|
@ -35,8 +35,17 @@ __all__ = [
|
||||||
def tmp_output_dir() -> Path:
|
def tmp_output_dir() -> Path:
|
||||||
path = Path(__file__).parent.resolve() / "__tmp__"
|
path = Path(__file__).parent.resolve() / "__tmp__"
|
||||||
if path.exists():
|
if path.exists():
|
||||||
shutil.rmtree(str(path))
|
for subdir in path.iterdir():
|
||||||
path.mkdir()
|
if subdir.name == 'git':
|
||||||
|
# don't wipe out git repos every time, they don't rly change
|
||||||
|
continue
|
||||||
|
elif subdir.is_file() and subdir.parent != path:
|
||||||
|
continue
|
||||||
|
elif subdir.is_file():
|
||||||
|
subdir.unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(str(subdir))
|
||||||
|
path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue