update to datacollection parameters model
This commit is contained in:
+28
-214
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user