mirror of
https://github.com/p2p-ld/numpydantic.git
synced 2024-11-10 00:34:29 +00:00
first draft of slot-based array :)
This commit is contained in:
parent
9906e6c507
commit
4d27d0f636
13 changed files with 1276 additions and 12 deletions
|
@ -8,3 +8,9 @@ pydanticgen
|
||||||
template
|
template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:caption: Experimental Formats
|
||||||
|
|
||||||
|
slotarray
|
||||||
|
```
|
||||||
|
|
||||||
|
|
361
docs/api/linkml/slotarray.md
Normal file
361
docs/api/linkml/slotarray.md
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
# Slot Arrays
|
||||||
|
|
||||||
|
Will explain further in the morning :)
|
||||||
|
|
||||||
|
See:
|
||||||
|
- [https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1925999203](https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1925999203)
|
||||||
|
- [https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1926195529](https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1926195529)
|
||||||
|
|
||||||
|
## Working Examples
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
ExactDimension:
|
||||||
|
description: exact anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions: 3
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class ExactDimension(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
exact anonymous dimensions
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: NDArray[Shape[*, *, *], Float] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
ExactNamedDimension:
|
||||||
|
description: Exact named dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
alias: longitude
|
||||||
|
t:
|
||||||
|
rank: 2
|
||||||
|
alias: time
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class ExactNamedDimension(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
Exact named dimensions
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: NDArray[Shape[* latitude, * longitude, * time], Float] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
MinDimensions:
|
||||||
|
description: Minimum anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 3
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class MinDimensions(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
Minimum anonymous dimensions
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: NDArray[Shape[*, *, *, ...], Float] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
MaxDimensions:
|
||||||
|
description: Maximum anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
max: 3
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class MaxDimensions(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
Maximum anonymous dimensions
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: Union[
|
||||||
|
NDArray[Shape["*"], Float],
|
||||||
|
NDArray[Shape["*, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *"], Float]
|
||||||
|
] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
RangeDimensions:
|
||||||
|
description: Range of anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 2
|
||||||
|
max: 5
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class RangeDimensions(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
Range of anonymous dimensions
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: Union[
|
||||||
|
NDArray[Shape["*, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *, *, *"], Float]
|
||||||
|
] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
ExactCardinality:
|
||||||
|
description: An axis with a specified cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality: 3
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class ExactCardinality(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
An axis with a specified cardinality
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: NDArray[Shape["3 x"], Float] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
MaxCardinality:
|
||||||
|
description: An axis with a maximum cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality:
|
||||||
|
max: 3
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class MaxCardinality(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
An axis with a maximum cardinality
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: Union[
|
||||||
|
NDArray[Shape["1 x"], Float],
|
||||||
|
NDArray[Shape["2 x"], Float],
|
||||||
|
NDArray[Shape["3 x"], Float]
|
||||||
|
] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
RangeCardinality:
|
||||||
|
description: An axis with a min and maximum cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality:
|
||||||
|
min: 2
|
||||||
|
max: 4
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class RangeCardinality(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
An axis with a min and maximum cardinality
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: Union[
|
||||||
|
NDArray[Shape["2 x"], Float],
|
||||||
|
NDArray[Shape["3 x"], Float],
|
||||||
|
NDArray[Shape["4 x"], Float]
|
||||||
|
] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
````{tab-item} YAML
|
||||||
|
```yaml
|
||||||
|
ExclusiveAxes:
|
||||||
|
description: Two mutually exclusive definitions of an axis that define its different forms
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
rgb:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 3
|
||||||
|
rgba:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 4
|
||||||
|
```
|
||||||
|
````
|
||||||
|
````{tab-item} Pydantic
|
||||||
|
```python
|
||||||
|
class ExclusiveAxes(ConfiguredBaseModel):
|
||||||
|
"""
|
||||||
|
Two mutually exclusive definitions of an axis that define its different forms
|
||||||
|
"""
|
||||||
|
linkml_meta: ClassVar[LinkML_Meta] = Field(LinkML_Meta(), frozen=True)
|
||||||
|
temp: Union[
|
||||||
|
NDArray[Shape["* x, * y, 3 rgb"], Float],
|
||||||
|
NDArray[Shape["* x, * y, 4 rgba"], Float]
|
||||||
|
] = Field(...)
|
||||||
|
```
|
||||||
|
````
|
||||||
|
`````
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
Any shape array
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
classes:
|
||||||
|
TemperatureDataset:
|
||||||
|
attributes:
|
||||||
|
temperatures_in_K:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
required: true
|
||||||
|
array:
|
||||||
|
```
|
||||||
|
|
||||||
|
One specified, named dimension, and any number of other dimensions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 1
|
||||||
|
# optionally, to be explicit:
|
||||||
|
max: null
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude_in_deg
|
||||||
|
```
|
||||||
|
|
||||||
|
Two required dimensions and two optional dimensions that will generate
|
||||||
|
a union of the combinatoric product of the optional dimensions.
|
||||||
|
Rank must be unspecified in optional dimensions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
z:
|
||||||
|
cardinality: 3
|
||||||
|
required: false
|
||||||
|
theta:
|
||||||
|
cardinality: 4
|
||||||
|
required: false
|
||||||
|
```
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: numpydantic.linkml.slotarray
|
||||||
|
:members:
|
||||||
|
```
|
|
@ -4,24 +4,35 @@ Isolated generator for array classes
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
||||||
|
|
||||||
from numpydantic.maps import flat_to_nptyping
|
from numpydantic.maps import flat_to_nptyping
|
||||||
|
|
||||||
|
SourceType = ClassDefinition | SlotDefinition
|
||||||
|
|
||||||
|
ArrayDefinitionType = Literal[
|
||||||
|
"SLOT", # defined by independent slots
|
||||||
|
"CLASS_SLOT", # defined by a class used as a slot's range
|
||||||
|
"CLASS", # defined by a whole class
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ArrayFormat(ABC):
|
class ArrayFormat(ABC):
|
||||||
"""
|
"""
|
||||||
Metaclass for different LinkML array source formats
|
Metaclass for different LinkML array source formats
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEFINITION_TYPE: ArrayDefinitionType = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_array(cls, cls_: ClassDefinition) -> bool:
|
def is_array(cls, cls_: SourceType) -> bool:
|
||||||
"""Check whether a given class matches one of our subclasses definitions"""
|
"""Check whether a given class matches one of our subclasses definitions"""
|
||||||
return any([subcls.check(cls_) for subcls in cls.__subclasses__()])
|
return any([subcls.check(cls_) for subcls in cls.__subclasses__()])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, cls_: ClassDefinition) -> type["ArrayFormat"]:
|
def get(cls, cls_: SourceType) -> type["ArrayFormat"]:
|
||||||
"""Get matching ArrayFormat subclass"""
|
"""Get matching ArrayFormat subclass"""
|
||||||
for subcls in cls.__subclasses__():
|
for subcls in cls.__subclasses__():
|
||||||
if subcls.check(cls_):
|
if subcls.check(cls_):
|
||||||
|
@ -29,12 +40,12 @@ class ArrayFormat(ABC):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def check(cls, cls_: ClassDefinition) -> bool:
|
def check(cls, cls_: SourceType) -> bool:
|
||||||
"""Method for array format subclasses to check if they match a given source class"""
|
"""Method for array format subclasses to check if they match a given source class"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def make(cls, cls_: ClassDefinition) -> str:
|
def make(cls, cls_: SourceType) -> str:
|
||||||
"""
|
"""
|
||||||
Make an annotation string from a given array format source class
|
Make an annotation string from a given array format source class
|
||||||
"""
|
"""
|
||||||
|
@ -43,15 +54,40 @@ class ArrayFormat(ABC):
|
||||||
class LinkMLNDArray(ArrayFormat):
|
class LinkMLNDArray(ArrayFormat):
|
||||||
"""
|
"""
|
||||||
Tentative linkml-arrays style NDArray
|
Tentative linkml-arrays style NDArray
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
TemperatureMatrix:
|
||||||
|
description: A 3D array of temperatures
|
||||||
|
implements:
|
||||||
|
- linkml:NDArray
|
||||||
|
- linkml:RowOrderedArray
|
||||||
|
attributes:
|
||||||
|
values:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
implements:
|
||||||
|
- linkml:elements
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
annotations:
|
||||||
|
dimensions: 3
|
||||||
|
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://github.com/linkml/linkml-model/blob/main/tests/input/examples/schema_definition-array-2.yaml
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(cls, cls_: ClassDefinition) -> bool:
|
def check(cls, cls_: SourceType) -> bool:
|
||||||
"""Check if linkml:NDArray in implements"""
|
"""Check if linkml:NDArray in implements"""
|
||||||
return "linkml:NDArray" in cls_.implements
|
return "linkml:NDArray" in cls_.implements
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, cls_: ClassDefinition) -> str:
|
def make(cls, cls_: SourceType) -> str:
|
||||||
"""Make NDArray"""
|
"""Make NDArray"""
|
||||||
raise NotImplementedError("Havent implemented NDArrays yet!")
|
raise NotImplementedError("Havent implemented NDArrays yet!")
|
||||||
|
|
||||||
|
@ -62,12 +98,12 @@ class LinkMLDataArray(ArrayFormat):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(cls, cls_: ClassDefinition) -> bool:
|
def check(cls, cls_: SourceType) -> bool:
|
||||||
"""Check if linkml:DataArray in implements"""
|
"""Check if linkml:DataArray in implements"""
|
||||||
return "linkml:DataArray" in cls_.implements
|
return "linkml:DataArray" in cls_.implements
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, cls_: ClassDefinition) -> str:
|
def make(cls, cls_: SourceType) -> str:
|
||||||
"""Make DataArray"""
|
"""Make DataArray"""
|
||||||
raise NotImplementedError("Havent generated DataArray types yet!")
|
raise NotImplementedError("Havent generated DataArray types yet!")
|
||||||
|
|
||||||
|
|
|
@ -468,6 +468,19 @@ class PydanticGenerator(BasePydanticGenerator):
|
||||||
|
|
||||||
return slot_value
|
return slot_value
|
||||||
|
|
||||||
|
def generate_python_range(
|
||||||
|
self, slot_range, slot_def: SlotDefinition, class_def: ClassDefinition
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Overridden to handle generating slot-only arrays
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
ArrayFormat.is_array(slot_def)
|
||||||
|
and ArrayFormat.get(slot_def).DEFINITION_TYPE == "SLOT"
|
||||||
|
):
|
||||||
|
return ArrayFormat.get(slot_def).make(slot_def)
|
||||||
|
return super().generate_python_range(slot_range, slot_def, class_def)
|
||||||
|
|
||||||
def serialize(self) -> str:
|
def serialize(self) -> str:
|
||||||
"""Generate LinkML models from schema!"""
|
"""Generate LinkML models from schema!"""
|
||||||
predefined_slot_values = {}
|
predefined_slot_values = {}
|
||||||
|
@ -603,7 +616,7 @@ class PydanticGenerator(BasePydanticGenerator):
|
||||||
underscore=underscore,
|
underscore=underscore,
|
||||||
enums=enums,
|
enums=enums,
|
||||||
predefined_slot_values=predefined_slot_values,
|
predefined_slot_values=predefined_slot_values,
|
||||||
allow_extra=self.allow_extra,
|
# allow_extra=self.allow_extra,
|
||||||
metamodel_version=self.schema.metamodel_version,
|
metamodel_version=self.schema.metamodel_version,
|
||||||
version=self.schema.version,
|
version=self.schema.version,
|
||||||
class_isa_plus_mixins=self.get_class_isa_plus_mixins(sorted_classes),
|
class_isa_plus_mixins=self.get_class_isa_plus_mixins(sorted_classes),
|
||||||
|
|
357
numpydantic/linkml/slotarray.py
Normal file
357
numpydantic/linkml/slotarray.py
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
"""
|
||||||
|
Experimental Slot-only NDArray specification
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1925999203
|
||||||
|
- https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1926195529
|
||||||
|
|
||||||
|
"""
|
||||||
|
import itertools
|
||||||
|
from dataclasses import dataclass, field, make_dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import jsonasobj2
|
||||||
|
from linkml_runtime.linkml_model import SlotDefinition as SlotDefinition_Base
|
||||||
|
|
||||||
|
from numpydantic.linkml import ArrayFormat
|
||||||
|
from numpydantic.maps import flat_to_nptyping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RangeAttr:
|
||||||
|
min: Optional[int] = None
|
||||||
|
max: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DimensionsAttr(RangeAttr):
|
||||||
|
"""Specification for number of axes in array"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CardinalityAttr(RangeAttr):
|
||||||
|
"""Specification for size of a given axis"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Axis:
|
||||||
|
"""Specification of individual axis within an NDArray"""
|
||||||
|
|
||||||
|
rank: Optional[int] = None
|
||||||
|
alias: Optional[str] = None
|
||||||
|
cardinality: Optional[int | CardinalityAttr] = None
|
||||||
|
required: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArraySlot:
|
||||||
|
"""Specification of an NDArray as a slot"""
|
||||||
|
|
||||||
|
axes: Optional[dict[str, Axis]] = None
|
||||||
|
dimensions: Optional[int | DimensionsAttr] = None
|
||||||
|
|
||||||
|
|
||||||
|
def patch_linkml() -> type["SlotDefinition_Base"]:
|
||||||
|
"""
|
||||||
|
Monkeypatch the LinkML runtime models to add the new properties to define a slotarray :)
|
||||||
|
"""
|
||||||
|
import linkml_runtime
|
||||||
|
from linkml_runtime.linkml_model.meta import SlotDefinition
|
||||||
|
from linkml_runtime.utils import schemaview
|
||||||
|
|
||||||
|
# make the new SlotDefinition...
|
||||||
|
newslot = make_dataclass(
|
||||||
|
"SlotDefinition",
|
||||||
|
[("array", Optional[ArraySlot], field(default=None))],
|
||||||
|
bases=(SlotDefinition,),
|
||||||
|
)
|
||||||
|
linkml_runtime.linkml_model.meta.SlotDefinition = newslot
|
||||||
|
linkml_runtime.linkml_model.SlotDefinition = newslot
|
||||||
|
schemaview.SlotDefinition = newslot
|
||||||
|
return newslot
|
||||||
|
|
||||||
|
|
||||||
|
# janky local patching until we get metamodel amended
|
||||||
|
SlotDefinition = patch_linkml()
|
||||||
|
|
||||||
|
|
||||||
|
class SlotArrayTemplate:
|
||||||
|
def __init__(self, slot: SlotDefinition):
|
||||||
|
self.slot = slot
|
||||||
|
|
||||||
|
def make(self) -> str:
|
||||||
|
array: ArraySlot = self.slot.array
|
||||||
|
|
||||||
|
shape = self._handle_shape(array)
|
||||||
|
dtype = self._handle_dtype(self.slot)
|
||||||
|
if isinstance(shape, list):
|
||||||
|
shape = [f'NDArray[Shape["{a_shape}"], {dtype}]' for a_shape in shape]
|
||||||
|
union = "Union[\n" + " " * 8
|
||||||
|
union += (",\n" + " " * 8).join(shape)
|
||||||
|
union += "\n" + " " * 4 + "]"
|
||||||
|
template = union
|
||||||
|
else:
|
||||||
|
template = f'NDArray[Shape["{shape}"], {dtype}]'
|
||||||
|
return template
|
||||||
|
|
||||||
|
def _handle_shape(self, array: ArraySlot) -> str | list[str]:
|
||||||
|
# If we have no axes or dimensions, any shape array
|
||||||
|
if (
|
||||||
|
getattr(array, "axes", None) is None
|
||||||
|
and getattr(array, "dimensions", None) is None
|
||||||
|
):
|
||||||
|
return "*, ..."
|
||||||
|
elif getattr(array, "axes", None) is None:
|
||||||
|
return self._shape_from_dimension(array)
|
||||||
|
elif getattr(array, "dimensions", None) is None:
|
||||||
|
return self._shape_from_axes(array)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Joint axis and dimension arrays")
|
||||||
|
|
||||||
|
def _shape_from_dimension(self, array: ArraySlot):
|
||||||
|
# just dimensions
|
||||||
|
if isinstance(array.dimensions, int):
|
||||||
|
# exact number of unnamed dimensions
|
||||||
|
return ", ".join(["*"] * array.dimensions)
|
||||||
|
elif (
|
||||||
|
getattr(array.dimensions, "min", None) is None
|
||||||
|
and getattr(array.dimensions, "max", None) is None
|
||||||
|
):
|
||||||
|
return "*, ..."
|
||||||
|
elif (
|
||||||
|
getattr(array.dimensions, "min", None) is not None
|
||||||
|
and getattr(array.dimensions, "max", None) is None
|
||||||
|
):
|
||||||
|
return ", ".join(["*"] * array.dimensions.min) + ", ..."
|
||||||
|
else:
|
||||||
|
min = (
|
||||||
|
1
|
||||||
|
if getattr(array.dimensions, "min", None) is None
|
||||||
|
else array.dimensions.min
|
||||||
|
)
|
||||||
|
return [", ".join(["*"] * i) for i in range(min, array.dimensions.max + 1)]
|
||||||
|
|
||||||
|
def _shape_from_axes(self, array: ArraySlot):
|
||||||
|
# just axes!
|
||||||
|
# FIXME: what in the hell is the matter with jsonasobj2 lmao
|
||||||
|
axes = {
|
||||||
|
name: Axis(**axis) for name, axis in jsonasobj2.as_dict(array.axes).items()
|
||||||
|
}
|
||||||
|
indices = [
|
||||||
|
ax.rank for ax in axes.values() if getattr(ax, "rank", None) is not None
|
||||||
|
]
|
||||||
|
# TODO: Sort by rank first
|
||||||
|
|
||||||
|
if len(indices) == len(axes) and len(set(indices)) < len(axes):
|
||||||
|
# mutually exclusive axes
|
||||||
|
# FIXME: it's getting to be the braindead hours, this is a total nightmare of a way to check dupe idx
|
||||||
|
# for the love of god why. do it better lol. get some food and go to bed. very last thing
|
||||||
|
ax_defs = {}
|
||||||
|
for name, ax in axes.items():
|
||||||
|
if ax.rank not in ax_defs:
|
||||||
|
ax_defs[ax.rank] = []
|
||||||
|
ax_defs[ax.rank].append(self._make_axis(name, ax))
|
||||||
|
ax_defs = list(ax_defs.values())
|
||||||
|
elif len(indices) < len(axes):
|
||||||
|
raise NotImplementedError("Optional Axes")
|
||||||
|
else:
|
||||||
|
# independently defined axes, these are our friends :)
|
||||||
|
ax_defs = [self._make_axis(name, ax) for name, ax in axes.items()]
|
||||||
|
|
||||||
|
if all([isinstance(ax_def, str) for ax_def in ax_defs]):
|
||||||
|
# no expansion needed
|
||||||
|
return ", ".join(ax_defs)
|
||||||
|
# expand such that for each axis, we get all possible combinations
|
||||||
|
ax_defs = [
|
||||||
|
list(ax_def) if not isinstance(ax_def, list) else ax_def
|
||||||
|
for ax_def in ax_defs
|
||||||
|
]
|
||||||
|
return [", ".join(inner_def) for inner_def in itertools.product(*ax_defs)]
|
||||||
|
|
||||||
|
def _make_axis(self, name: str, ax: Axis) -> str | list[str]:
|
||||||
|
if getattr(ax, "alias", None):
|
||||||
|
name = ax.alias
|
||||||
|
name = name.lower()
|
||||||
|
|
||||||
|
if getattr(ax, "rank", None) is not None and not ax.required:
|
||||||
|
# FIXME: Do this in class validation
|
||||||
|
raise ValueError("Optional axes cannot be given explicit ranks")
|
||||||
|
|
||||||
|
if getattr(ax, "cardinality", None) is None:
|
||||||
|
return f"* {name}"
|
||||||
|
elif isinstance(ax.cardinality, int):
|
||||||
|
return f"{ax.cardinality} {name}"
|
||||||
|
else:
|
||||||
|
# FIXME: i remember why i hate dataclasses and love pydantic now lol - dataclass creation is not recursive
|
||||||
|
min = ax.cardinality.get("min", 1)
|
||||||
|
max = ax.cardinality.get("max", None)
|
||||||
|
if max is None:
|
||||||
|
raise ValueError("Cannot set minimum cardinality without maximum")
|
||||||
|
return [f"{i} {name}" for i in range(min, max + 1)]
|
||||||
|
|
||||||
|
def _handle_dtype(self, slot: SlotDefinition) -> str:
|
||||||
|
# TODO: handle any_of, multiple typed arrays
|
||||||
|
return flat_to_nptyping[slot.range]
|
||||||
|
|
||||||
|
|
||||||
|
class SlotNDArray(ArrayFormat):
|
||||||
|
"""
|
||||||
|
Prospective array format specified as a slot/attribute alone
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Any shape array
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
classes:
|
||||||
|
TemperatureDataset:
|
||||||
|
attributes:
|
||||||
|
temperatures_in_K:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
required: true
|
||||||
|
array:
|
||||||
|
|
||||||
|
|
||||||
|
Exactly 3 unspecified axes/dimensions
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
classes:
|
||||||
|
TemperatureDataset:
|
||||||
|
attributes:
|
||||||
|
temperatures_in_K:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions: 3
|
||||||
|
|
||||||
|
Exactly 3 named dimensions
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude_in_deg
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
alias: longitude_in_deg
|
||||||
|
t:
|
||||||
|
rank: 2
|
||||||
|
alias: time_in_d
|
||||||
|
|
||||||
|
At least three, at most 5, and between 3 and 5 anonymous dimensions
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 3
|
||||||
|
|
||||||
|
# or
|
||||||
|
dimensions:
|
||||||
|
max: 5
|
||||||
|
|
||||||
|
# or
|
||||||
|
dimensions:
|
||||||
|
min: 3
|
||||||
|
max: 5
|
||||||
|
|
||||||
|
One specified, named dimension, and any number of other dimensions
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 1
|
||||||
|
# optionally, to be explicit:
|
||||||
|
max: null
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude_in_deg
|
||||||
|
|
||||||
|
Axes with specified cardinality
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality: 3
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
cardinality:
|
||||||
|
min: 2
|
||||||
|
z:
|
||||||
|
rank: 2
|
||||||
|
cardinality:
|
||||||
|
min: 3
|
||||||
|
max: 5
|
||||||
|
|
||||||
|
Two required dimensions and two optional dimensions that will generate
|
||||||
|
a union of the combinatoric product of the optional dimensions.
|
||||||
|
Rank must be unspecified in optional dimensions
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
z:
|
||||||
|
cardinality: 3
|
||||||
|
required: false
|
||||||
|
theta:
|
||||||
|
cardinality: 4
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
|
Two required dimensions with a third dimension with two mutually exclusive forms
|
||||||
|
as indicated by their equal rank
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
rgb:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 3
|
||||||
|
rgba:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1925999203
|
||||||
|
- https://github.com/linkml/linkml-arrays/issues/7#issuecomment-1926195529
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFINITION_TYPE = "SLOT"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, SlotDefinition) -> bool:
|
||||||
|
if getattr(SlotDefinition, "array", None) is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make(cls, cls_: SlotDefinition) -> str:
|
||||||
|
return SlotArrayTemplate(cls_).make()
|
|
@ -32,7 +32,7 @@ Arrays larger than this size (in bytes) will be compressed and b64 encoded when
|
||||||
serializing to JSON.
|
serializing to JSON.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ARRAY_TYPES = np.ndarray | DaskArray | NDArrayProxy
|
ARRAY_TYPES = Union[np.ndarray, DaskArray, NDArrayProxy]
|
||||||
|
|
||||||
|
|
||||||
def list_of_lists_schema(shape: Shape, array_type_handler: dict) -> ListSchema:
|
def list_of_lists_schema(shape: Shape, array_type_handler: dict) -> ListSchema:
|
||||||
|
|
|
@ -101,7 +101,7 @@ select = [
|
||||||
|
|
||||||
]
|
]
|
||||||
ignore = [
|
ignore = [
|
||||||
"ANN101", "ANN102"
|
"ANN101", "ANN102", "UP007"
|
||||||
]
|
]
|
||||||
|
|
||||||
fixable = ["ALL"]
|
fixable = ["ALL"]
|
||||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
206
tests/data/slotarray.yaml
Normal file
206
tests/data/slotarray.yaml
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
id: https://example.org/arrays
|
||||||
|
name: arrays-temperature-example
|
||||||
|
title: Array Temperature Example
|
||||||
|
description: |-
|
||||||
|
Example implementation of a slot-only NDArray
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
wgs84: http://www.w3.org/2003/01/geo/wgs84_pos#
|
||||||
|
example: https://example.org/
|
||||||
|
|
||||||
|
default_prefix: example
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
|
||||||
|
classes:
|
||||||
|
ExactDimension:
|
||||||
|
description: exact anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions: 3
|
||||||
|
|
||||||
|
ExactNamedDimension:
|
||||||
|
description: Exact named dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
alias: longitude
|
||||||
|
t:
|
||||||
|
rank: 2
|
||||||
|
alias: time
|
||||||
|
|
||||||
|
MinDimensions:
|
||||||
|
description: Minimum anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 3
|
||||||
|
|
||||||
|
MaxDimensions:
|
||||||
|
description: Maximum anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
max: 3
|
||||||
|
|
||||||
|
RangeDimensions:
|
||||||
|
description: Range of anonymous dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 2
|
||||||
|
max: 5
|
||||||
|
|
||||||
|
MixedNamedUnnamed:
|
||||||
|
description: One specified, named dimension, and any number of other dimensions
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
dimensions:
|
||||||
|
min: 1
|
||||||
|
# optionally, to be explicit:
|
||||||
|
max: null
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
alias: latitude_in_deg
|
||||||
|
|
||||||
|
ExactCardinality:
|
||||||
|
description: An axis with a specified cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality: 3
|
||||||
|
|
||||||
|
MinCardinality:
|
||||||
|
description: An axis with a minimum cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality:
|
||||||
|
min: 3
|
||||||
|
|
||||||
|
MaxCardinality:
|
||||||
|
description: An axis with a maximum cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality:
|
||||||
|
max: 3
|
||||||
|
|
||||||
|
RangeCardinality:
|
||||||
|
description: An axis with a min and maximum cardinality
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
cardinality:
|
||||||
|
min: 2
|
||||||
|
max: 4
|
||||||
|
|
||||||
|
OptionalAxes:
|
||||||
|
description: Two optional axes that can be present in any combination
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
z:
|
||||||
|
cardinality: 3
|
||||||
|
required: false
|
||||||
|
theta:
|
||||||
|
cardinality: 4
|
||||||
|
required: false
|
||||||
|
|
||||||
|
ExclusiveAxes:
|
||||||
|
description: Two mutually exclusive definitions of an axis that define its different forms
|
||||||
|
attributes:
|
||||||
|
temp:
|
||||||
|
range: float
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
array:
|
||||||
|
axes:
|
||||||
|
x:
|
||||||
|
rank: 0
|
||||||
|
y:
|
||||||
|
rank: 1
|
||||||
|
rgb:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 3
|
||||||
|
rgba:
|
||||||
|
rank: 2
|
||||||
|
cardinality: 4
|
||||||
|
|
118
tests/data/temperature_model.yaml
Normal file
118
tests/data/temperature_model.yaml
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
id: https://example.org/arrays
|
||||||
|
name: arrays-temperature-example
|
||||||
|
title: Array Temperature Example
|
||||||
|
description: |-
|
||||||
|
from https://github.com/linkml/linkml-arrays/blob/ee2a063dbc6302c67506a2227ea9a5cc048f7584/tests/input/temperature_dataset.yaml
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
prefixes:
|
||||||
|
linkml: https://w3id.org/linkml/
|
||||||
|
wgs84: http://www.w3.org/2003/01/geo/wgs84_pos#
|
||||||
|
example: https://example.org/
|
||||||
|
|
||||||
|
default_prefix: example
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- linkml:types
|
||||||
|
|
||||||
|
classes:
|
||||||
|
|
||||||
|
TemperatureDataset:
|
||||||
|
tree_root: true
|
||||||
|
implements:
|
||||||
|
- linkml:DataArray
|
||||||
|
attributes:
|
||||||
|
name:
|
||||||
|
identifier: true
|
||||||
|
range: string
|
||||||
|
latitude_in_deg:
|
||||||
|
implements:
|
||||||
|
- linkml:axis
|
||||||
|
range: LatitudeSeries
|
||||||
|
required: true
|
||||||
|
annotations:
|
||||||
|
axis_index: 0
|
||||||
|
longitude_in_deg:
|
||||||
|
implements:
|
||||||
|
- linkml:axis
|
||||||
|
range: LongitudeSeries
|
||||||
|
required: true
|
||||||
|
annotations:
|
||||||
|
axis_index: 1
|
||||||
|
time_in_d:
|
||||||
|
implements:
|
||||||
|
- linkml:axis
|
||||||
|
range: DaySeries
|
||||||
|
required: true
|
||||||
|
annotations:
|
||||||
|
axis_index: 2
|
||||||
|
temperatures_in_K:
|
||||||
|
implements:
|
||||||
|
- linkml:array
|
||||||
|
range: TemperatureMatrix
|
||||||
|
required: true
|
||||||
|
|
||||||
|
TemperatureMatrix:
|
||||||
|
description: A 3D array of temperatures
|
||||||
|
implements:
|
||||||
|
- linkml:NDArray
|
||||||
|
- linkml:RowOrderedArray
|
||||||
|
attributes:
|
||||||
|
values:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
implements:
|
||||||
|
- linkml:elements
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: K
|
||||||
|
annotations:
|
||||||
|
dimensions: 3
|
||||||
|
|
||||||
|
LatitudeSeries:
|
||||||
|
description: A series whose values represent latitude
|
||||||
|
implements:
|
||||||
|
- linkml:NDArray
|
||||||
|
attributes:
|
||||||
|
values:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
implements:
|
||||||
|
- linkml:elements
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: deg
|
||||||
|
annotations:
|
||||||
|
dimensions: 1
|
||||||
|
|
||||||
|
LongitudeSeries:
|
||||||
|
description: A series whose values represent longitude
|
||||||
|
implements:
|
||||||
|
- linkml:NDArray
|
||||||
|
attributes:
|
||||||
|
values:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
implements:
|
||||||
|
- linkml:elements
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: deg
|
||||||
|
annotations:
|
||||||
|
dimensions: 1
|
||||||
|
|
||||||
|
DaySeries:
|
||||||
|
description: A series whose values represent the days since the start of the measurement period
|
||||||
|
implements:
|
||||||
|
- linkml:NDArray
|
||||||
|
attributes:
|
||||||
|
values:
|
||||||
|
range: float
|
||||||
|
multivalued: true
|
||||||
|
implements:
|
||||||
|
- linkml:elements
|
||||||
|
required: true
|
||||||
|
unit:
|
||||||
|
ucum_code: d
|
||||||
|
annotations:
|
||||||
|
dimensions: 1
|
|
@ -4,6 +4,8 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent / "data"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmp_output_dir() -> Path:
|
def tmp_output_dir() -> Path:
|
||||||
|
@ -72,3 +74,29 @@ def nwb_linkml_array() -> tuple[ClassDefinition, str]:
|
||||||
NDArray[Shape["* x, * y, 3 z, 4 a"], Number]
|
NDArray[Shape["* x, * y, 3 z, 4 a"], Number]
|
||||||
]"""
|
]"""
|
||||||
return classdef, generated
|
return classdef, generated
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def patch_slotarray():
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# modname = "linkml_runtime.linkml_model.meta"
|
||||||
|
# if modname in sys.modules:
|
||||||
|
# del sys.modules[modname]
|
||||||
|
|
||||||
|
from numpydantic.linkml.slotarray import patch_linkml
|
||||||
|
import linkml_runtime
|
||||||
|
from linkml_runtime.linkml_model.meta import SlotDefinition
|
||||||
|
|
||||||
|
original_slot = SlotDefinition
|
||||||
|
|
||||||
|
yield patch_linkml()
|
||||||
|
|
||||||
|
linkml_runtime.linkml_model.SlotDefinition = original_slot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def slotarray_schemaview(patch_slotarray):
|
||||||
|
from linkml_runtime.utils.schemaview import SchemaView
|
||||||
|
|
||||||
|
return SchemaView(DATA_DIR / "slotarray.yaml")
|
||||||
|
|
139
tests/test_linkml/test_slotarray.py
Normal file
139
tests/test_linkml/test_slotarray.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
"""
|
||||||
|
Lol i know all these shoudl be made into parameterized test cases and i will do that after
|
||||||
|
i get them to work. don't @ me bro
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pdb
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..fixtures import patch_slotarray, slotarray_schemaview, DATA_DIR
|
||||||
|
|
||||||
|
from numpydantic.linkml.slotarray import SlotNDArray
|
||||||
|
from numpydantic.linkml.pydanticgen import PydanticGenerator
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Only dimensions
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_dimensions(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("ExactDimension")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
# hardcoding for now...
|
||||||
|
assert annotation == 'NDArray[Shape["*, *, *"], Float]'
|
||||||
|
|
||||||
|
|
||||||
|
def test_min_dimensions(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("MinDimensions")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert annotation == 'NDArray[Shape["*, *, *, ..."], Float]'
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_dimensions(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("MaxDimensions")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert (
|
||||||
|
annotation
|
||||||
|
== """Union[
|
||||||
|
NDArray[Shape["*"], Float],
|
||||||
|
NDArray[Shape["*, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *"], Float]
|
||||||
|
]"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_range_dimensions(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("RangeDimensions")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert (
|
||||||
|
annotation
|
||||||
|
== """Union[
|
||||||
|
NDArray[Shape["*, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *, *"], Float],
|
||||||
|
NDArray[Shape["*, *, *, *, *"], Float]
|
||||||
|
]"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_cardinality(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("ExactCardinality")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert annotation == """NDArray[Shape["3 x"], Float]"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_min_cardinality(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("MinCardinality")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
# no way to specify ranges in nptyping ranges yet, this would go to infinity
|
||||||
|
SlotNDArray.make(array)
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_cardinality(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("MaxCardinality")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert (
|
||||||
|
annotation
|
||||||
|
== """Union[
|
||||||
|
NDArray[Shape["1 x"], Float],
|
||||||
|
NDArray[Shape["2 x"], Float],
|
||||||
|
NDArray[Shape["3 x"], Float]
|
||||||
|
]"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_range_cardinality(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("RangeCardinality")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
annotation
|
||||||
|
== """Union[
|
||||||
|
NDArray[Shape["2 x"], Float],
|
||||||
|
NDArray[Shape["3 x"], Float],
|
||||||
|
NDArray[Shape["4 x"], Float]
|
||||||
|
]"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclusive_axes(slotarray_schemaview):
|
||||||
|
cls = slotarray_schemaview.get_class("ExclusiveAxes")
|
||||||
|
array = cls.attributes["temp"]
|
||||||
|
annotation = SlotNDArray.make(array)
|
||||||
|
assert (
|
||||||
|
annotation
|
||||||
|
== """Union[
|
||||||
|
NDArray[Shape["* x, * y, 3 rgb"], Float],
|
||||||
|
NDArray[Shape["* x, * y, 4 rgba"], Float]
|
||||||
|
]"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# def test_generate_schema(patch_slotarray):
|
||||||
|
# schema = DATA_DIR / "slotarray.yaml"
|
||||||
|
# generator = PydanticGenerator(schema)
|
||||||
|
# serialized = generator.serialize()
|
||||||
|
# pdb.set_trace()
|
||||||
|
|
||||||
|
|
||||||
|
# def test_optional_axes(slotarray_schemaview):
|
||||||
|
# cls = slotarray_schemaview.get_class("OptionalAxes")
|
||||||
|
# array = cls.attributes["temp"]
|
||||||
|
# annotation = SlotNDArray.make(array)
|
||||||
|
# # assert annotation == ""
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# def test_mixed_named_unnamed_dimensions(slotarray_schemaview):
|
||||||
|
# cls = slotarray_schemaview.get_class("MixedNamedUnnamed")
|
||||||
|
# array = cls.attributes["temp"]
|
||||||
|
# annotation = SlotNDArray.make(array)
|
||||||
|
# # assert annotation == ""
|
Loading…
Reference in a new issue