update to datacollection parameters model
Build and Publish / test (push) Failing after 1m39s
Build and Publish / build (push) Skipped
Build and Publish / Build and Deploy Docs (push) Skipped

This commit is contained in:
GotthardG
2026-06-09 09:56:42 +02:00
parent 4322720f8f
commit 07352f8d00
+28 -214
View File
@@ -3,7 +3,7 @@ import re
from enum import Enum
from typing import Annotated, Literal, Tuple, List, Optional
from dataclasses import dataclass
from pydantic import BaseModel, Field, field_validator, AfterValidator, ConfigDict
from pydantic import BaseModel, Field, field_validator, AfterValidator, ConfigDict, AliasChoices
from aare.common.coordinate import Coordinate, positive_coords
from aare.common.diffraction_geometry import DiffractionGeometry
@@ -54,22 +54,32 @@ class PuckLoadedInfo(BaseModel):
class DataCollectionParameters(BaseModel):
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
directory: Optional[str] = None
oscillation: Optional[float] = None # Only accept positive float
exposure: Optional[float] = None # Only accept positive floats between 0 and 1
totalrange: Optional[int] = None # Only accept positive integers between 0 and 360
totalangle: Optional[int] = Field( # was totalrange
default=None,
validation_alias=AliasChoices('totalangle', 'totalrange')
) # Only accept positive integers between 0 and 360
transmission: Optional[int] = None # Only accept positive integers between 0 and 100
targetresolution: Optional[float] = None # Only accept positive float
beamsize: Optional[str] = None
aperture: Optional[int] = None # Optional string field
datacollectiontype: Optional[str] = None # Only accept "standard", other types might be added later
processingpipeline: Optional[str] = "" # Only accept "gopy", "autoproc", "xia2dials"
spacegroupnumber: Optional[int] = None # Only accept positive integers between 1 and 230
cellparameters: Optional[str] = None # Must be a set of six positive floats or integers
unitcell: Optional[str] = Field( # was cellparameters
default=None,
validation_alias=AliasChoices('unitcell', 'cellparameters')
) # Must be a set of six positive floats or integers
rescutkey: Optional[str] = None # Only accept "is" or "cchalf"
rescutvalue: Optional[float] = None # Must be a positive float if rescutkey is provided
userresolution: Optional[float] = None
processingresolution: Optional[float] = Field( # was userresolution
default=None,
validation_alias=AliasChoices('processingresolution', 'userresolution')
)
pdbid: Optional[str] = "" # Accepts either the format of the protein data bank code or {provided}
autoprocfull: Optional[bool] = None
procfull: Optional[bool] = None
@@ -83,10 +93,6 @@ class DataCollectionParameters(BaseModel):
cloud: bool = True
pdbmodel: Optional[str] = None
def to_dict(self):
"""Convert the model instance to a dictionary."""
return self.model_dump(exclude_unset=True)
@field_validator("directory", mode="after")
@classmethod
def directory_characters(cls, v):
@@ -105,13 +111,16 @@ class DataCollectionParameters(BaseModel):
# Validate directory pattern with macros and allowed characters
valid_macros = [
"{date}",
"{prefix}",
"{sgPuck}",
"{sgPosition}",
# Current macros
"{puck}",
"{position}",
"{prefix}",
"{date}",
"{run}",
"{beamline}",
# Legacy macros — accepted for back-compat, no longer documented
"{sgPuck}",
"{sgPosition}",
"{sgPrefix}",
"{sgPriority}",
"{protein}",
@@ -134,220 +143,25 @@ class DataCollectionParameters(BaseModel):
)
return v
@field_validator("aperture", mode="before")
@field_validator("unitcell", mode="before")
@classmethod
def aperture_selection(cls, v):
if v is not None:
try:
v = int(float(v))
if v not in {1, 2, 3}:
raise ValueError(f" '{v}' is not valid. Value must be 1, 2, or 3.")
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be 1, 2, or 3."
) from e
return v
@field_validator("oscillation", mode="before")
@classmethod
def positive_float_validator(cls, v):
if v is None:
return None
try:
v = float(v)
if v <= 0:
raise ValueError(f"'{v}' is not valid. Value must be a positive float.")
except (ValueError, TypeError) as e:
raise ValueError(
f"'{v}' is not valid. Value must be a positive float."
) from e
return v
@field_validator("exposure", mode="before")
@classmethod
def exposure_in_range(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 1):
raise ValueError(
f" '{v}' is not valid. Value must be a float between 0 and 1."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be a float between 0 and 1."
) from e
return v
@field_validator("totalrange", mode="before")
@classmethod
def totalrange_in_range(cls, v):
if v is not None:
try:
v = int(v)
if not (0 <= v <= 360):
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 0 and 360."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 0 and 360."
) from e
return v
@field_validator("transmission", mode="before")
@classmethod
def transmission_fraction(cls, v):
if v is not None:
try:
v = int(v)
if not (0 <= v <= 100):
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 0 and 100."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 0 and 100."
) from e
return v
@field_validator("datacollectiontype", mode="before")
@classmethod
def datacollectiontype_allowed(cls, v):
allowed = {"standard"} # Other types of data collection might be added later
if v and v.lower() not in allowed:
raise ValueError(f" '{v}' is not valid." f"Value must be one of {allowed}.")
return v
@field_validator("processingpipeline", mode="before")
@classmethod
def processingpipeline_allowed(cls, v):
allowed = {"aareproc", "autoproc"}
if v and v.lower() not in allowed:
raise ValueError(f" '{v}' is not valid." f"Value must be one of {allowed}.")
return v
@field_validator("spacegroupnumber", mode="before")
@classmethod
def spacegroupnumber_allowed(cls, v):
if v is not None:
try:
v = int(v)
if not (1 <= v <= 230):
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 1 and 230."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid."
f"Value must be an integer between 1 and 230."
) from e
return v
@field_validator("cellparameters", mode="before")
@classmethod
def cellparameters_format(cls, v):
def unitcell_format(cls, v):
if v:
# Replace commas with spaces, then split on whitespace
tokens = v.replace(",", " ").split()
try:
values = [float(i) for i in tokens]
except ValueError:
raise ValueError(
f" '{v}' is not valid."
" Value must be a set of six positive floats"
" or integers (separated by space or comma)."
f"'{v}' is not valid. "
"Value must be six positive floats or integers (space or comma separated)."
)
if len(values) != 6 or any(val <= 0 for val in values):
raise ValueError(
f" '{v}' is not valid."
" Value must be a set of six positive floats"
" or integers (separated by space or comma)."
f"'{v}' is not valid. "
"Value must be six positive floats or integers (space or comma separated)."
)
return v
# @field_validator("rescutkey", "rescutvalue", mode="before")
# @classmethod
# def rescutkey_value_pair(cls, values):
# rescutkey = values.get("rescutkey")
# rescutvalue = values.get("rescutvalue")
# if rescutkey and rescutvalue:
# if rescutkey not in {"is", "cchalf"}:
# raise ValueError("Rescutkey must be either 'is' or 'cchalf'")
# if not isinstance(rescutvalue, float) or rescutvalue <= 0:
# raise ValueError(
# "Rescutvalue must be a positive float if rescutkey is provided"
# )
# return values
@field_validator("trustedhigh", mode="before")
@classmethod
def trustedhigh_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 2.0):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a float between 0 and 2.0."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid." f"Value must be a float between 0 and 2.0."
) from e
return v
@field_validator("chiphiangles", mode="before")
@classmethod
def chiphiangles_allowed(cls, v):
if v is not None:
try:
v = float(v)
if not (0 <= v <= 30):
raise ValueError(
f" '{v}' is not valid."
f"Value must be a float between 0 and 30."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be a float between 0 and 30."
) from e
return v
@field_validator("dose", mode="before")
@classmethod
def dose_positive(cls, v):
if v is not None:
try:
v = float(v)
if v <= 0:
raise ValueError(
f" '{v}' is not valid. Value must be a positive float."
)
except (ValueError, TypeError) as e:
raise ValueError(
f" '{v}' is not valid. Value must be a positive float."
) from e
return v
@field_validator("pdbmodel", mode="after")
@classmethod
def validate_filepath(cls, v):
if v is None:
return v
v_str = str(v) # Ensure v is a string for further checks
if any(c in v_str for c in '<>:"|?*'):
raise ValueError("File path contains invalid characters.")
path = Path(v_str)
if not path.parts:
raise ValueError("Not a valid path.")
return v_str # Return as string for JSON serialization
@field_validator("cloud", mode="before")
@classmethod
def coerce_cloud_default(cls, v):