From 07352f8d002d175fee3aa25121fdd5e67c128fed Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:57:28 +0200 Subject: [PATCH] update to datacollection parameters model --- src/aare/common/models.py | 242 +++++--------------------------------- 1 file changed, 28 insertions(+), 214 deletions(-) diff --git a/src/aare/common/models.py b/src/aare/common/models.py index bfad1976..0c599b2d 100644 --- a/src/aare/common/models.py +++ b/src/aare/common/models.py @@ -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):