Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions sasdata/abscissa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from abc import ABC, abstractmethod

import numpy as np
from numpy._typing import ArrayLike

from sasdata.exceptions import InterpretationError
from sasdata.quantities.quantity import Quantity
from sasdata.util import is_increasing


class Abscissa(ABC):

def __init__(self, axes: list[Quantity]):
self._axes = axes
self._dimensionality = len(axes)

@property
def dimensionality(self) -> int:
""" Dimensionality of this data """
return self._dimensionality

@property
@abstractmethod
def is_grid(self) -> bool:
""" Are these coordinates using a grid representation
(as opposed to a general list representation)

is_grid = True: implies that the corresponding ordinate is n-dimensional tensor
is_grid = False: implies that the corresponding ordinate is a 1D list

If the data is one dimensional, is_grid=True
"""

@property
def axes(self) -> list[Quantity]:
""" Axes of the data:

If it's an (n1-by-n2-by-n3...) grid (is_grid=True): give the values for each axis, returning a list like
[Quantity(length n1), Quantity(length n2), Quantity(length n3) ... ]

If it is not grid data (is_grid=False), but n points on a general mesh, give one array for each dimension
[Quantity(length n), Quantity(length n), Quantity(length n) ... ]
"""

return self._axes

@staticmethod
def _determine_error_message(axis_arrays: list[np.ndarray], ordinate_shape: tuple):
""" Error message for the `.determine` function"""

shape_string = ", ".join([str(axis.shape) for axis in axis_arrays])

return f"Cannot interpret array shapes axis: [{shape_string}], ordinate: {ordinate_shape}"

@staticmethod
def determine(axis_data: list[Quantity[ArrayLike]], ordinate_data: Quantity[ArrayLike]) -> "Abscissa":
""" Get an Abscissa object that fits the combination of axes and data"""

# Different posibilites:
# 1: axes_data[i].shape == axes_data[j].shape == ordinate_data.shape
# 1a: axis_data[i] is 1D =>
# 1a-i: len(axes_data) == 1 => Grid type or Scatter type depending on sortedness
# 1a-ii: len(axes_data) != 1 => Scatter type
# 1b: axis_data[i] dimensionality matches len(axis_data) => Meshgrid type
# 1c: other => Error
# 2: (len(axes_data[0]), len(axes_data[1])... ) == ordinate_data.shape => Grid type
# 3: None of the above => Error

ordinate_shape = np.array(ordinate_data.value).shape
axis_arrays = [np.array(axis.value) for axis in axis_data]

# 1:
if all([axis.shape == ordinate_shape for axis in axis_arrays]):
# 1a:
if all([len(axis.shape)== 1 for axis in axis_arrays]):
# 1a-i:
if len(axis_arrays) == 1:
# Is it sorted
if is_increasing(axis_arrays[0]):
return GridAbscissa(axis_data)
else:
return ScatterAbscissa(axis_data)
# 1a-ii
else:
return ScatterAbscissa(axis_data)
# 1b
elif all([len(axis.shape) == len(axis_arrays) for axis in axis_arrays]):

return MeshgridAbscissa(axis_data)

else:
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))

elif all([len(axis.shape)== 1 for axis in axis_arrays]) and \
tuple([axis.shape[0] for axis in axis_arrays]) == ordinate_shape:

# Require that they are sorted
if all([is_increasing(axis) for axis in axis_arrays]):

return GridAbscissa(axis_data)

else:
raise InterpretationError("Grid axes are not sorted")

else:
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))

class GridAbscissa(Abscissa):

@property
def is_grid(self):
return True

class MeshgridAbscissa(Abscissa):

@property
def is_grid(self):
return True

class ScatterAbscissa(Abscissa):

@property
def is_grid(self):
return False
96 changes: 67 additions & 29 deletions sasdata/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,68 @@

from sasdata import dataset_types
from sasdata.dataset_types import DatasetType
from sasdata.metadata import Metadata, MetadataEncoder
from sasdata.metadata import DerivedMetadata, Metadata, MetadataEncoder
from sasdata.quantities.quantity import Quantity


class SasData:
""" General object containing data in the SasView ecosystem"""

def __init__(self,
name: str,
ordinate: Quantity,
abscissae: list[Quantity],
mask: Quantity,
dependents: list["SasData"],
metadata: Metadata):

self.name = name
self._ordinate = ordinate
self._abscissae = abscissae
self._mask = mask
self.dependents = dependents
self.metadata = metadata

@property
def ordinate(self) -> Quantity:
return self._ordinate

@property
def abscissae(self) -> list[Quantity]:
return self._abscissae

@property
def mask(self) -> Quantity:
return self._mask

def scatter_data(self):
""" Return data in the coordinate/value form [(x1, x2, x3, y)...]"""


class SasDerivedMeasurement(SasData):
""" General object sas measurement that has not come directly from a file,
for example, the difference between two datasets"""


def __init__(self,
name: str,
ordinate: Quantity,
abscissae: list[Quantity],
mask: Quantity,
dependents: list["SasData"],
metadata: DerivedMetadata):

super().__init__(
name=name,
ordinate=ordinate,
abscissae=abscissae,
mask=mask,
dependents=dependents,
metadata=metadata)


class SasMeasurement(SasData):

def __init__(
self,
name: str,
Expand All @@ -32,7 +89,7 @@ def __init__(
self.dataset_type: DatasetType = dataset_type

# Components that need to be organised after creation
self.mask = None # TODO: fill out
self._mask = None # TODO: fill out
self.model_requirements = None # TODO: fill out

# TODO: Handle the other data types.
Expand All @@ -52,32 +109,15 @@ def ordinate(self) -> Quantity:
def abscissae(self) -> Quantity:
match self.dataset_type:
case dataset_types.one_dim:
return self._data_contents["Q"]
return [self._data_contents["Q"]]
case dataset_types.two_dim:
# Type hinting is a bit lacking. Assume each part of the zip is a scalar value.
data_contents = np.array(
list(
zip(
self._data_contents["Qx"].value,
self._data_contents["Qy"].value,
)
)
)
# Use this value to extract units etc. Assume they will be the same for Qy.
reference_data_content = self._data_contents["Qx"]
# TODO: If this is a derived quantity then we are going to lose that
# information.
#
# TODO: Won't work when there's errors involved. On reflection, we
# probably want to avoid creating a new Quantity but at the moment I
# can't see a way around it.
return Quantity(data_contents, reference_data_content.units, name=self._data_contents["Qx"].name, id_header=self._data_contents["Qx"]._id_header)
return [self._data_contents["Qx"], self._data_contents["Qy"]]
case dataset_types.angle_dim:
return self._data_contents["Phi"]
return [self._data_contents["Phi"]]
case dataset_types.sesans:
return self._data_contents["SpinEchoLength"]
return [self._data_contents["SpinEchoLength"]]
case _:
None
return None

def __getitem__(self, item: str):
return self._data_contents[item]
Expand All @@ -96,7 +136,7 @@ def summary(self, indent=" "):

@staticmethod
def from_json(obj):
return SasData(
return SasMeasurement(
name=obj["name"],
dataset_type=DatasetType(
name=obj["type"]["name"],
Expand All @@ -119,7 +159,6 @@ def _save_h5(self, sasentry: HDF5Group):
for idx, (key, sasdata) in enumerate(self._data_contents.items()):
sasdata.as_h5(group, key)


@staticmethod
def save_h5(data: dict[str, typing.Self], path: str | typing.BinaryIO):
with h5py.File(path, "w") as f:
Expand All @@ -130,7 +169,6 @@ def save_h5(data: dict[str, typing.Self], path: str | typing.BinaryIO):
data._save_h5(sasentry)



class SasDataEncoder(MetadataEncoder):
def default(self, obj):
match obj:
Expand Down Expand Up @@ -174,7 +212,7 @@ def sasdata_reader2D_converter(data2d: SasData | None = None) -> SasData:
qx_data = new_x.flatten()
qy_data = new_y.flatten()
err_data = np.sqrt(data2d._data_contents["I"].variance.value)
if not data2d._data_contents["I"].has_variance or np.any(err_data <= 0):
if not data2d._data_contents["I"].has_error or np.any(err_data <= 0):
new_err_data = np.sqrt(np.abs(new_data))
else:
new_err_data = err_data.flatten()
Expand All @@ -184,6 +222,6 @@ def sasdata_reader2D_converter(data2d: SasData | None = None) -> SasData:
data2d._data_contents["I"].variance.value = new_err_data ** 2
data2d._data_contents["Qx"].value = qx_data
data2d._data_contents["Qy"].value = qy_data
data2d.mask = mask
data2d._mask = mask

return data2d
Loading
Loading