Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the documentation to Docusaurus 3.0 #177

Closed
wants to merge 18 commits into from
Closed
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
29 changes: 0 additions & 29 deletions .pre-commit-config.yaml

This file was deleted.

63 changes: 56 additions & 7 deletions chemex/configuration/base.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,76 @@
"""Utility module for list and string manipulation in Pydantic models.

This module provides utility functions for ensuring a variable is in list format and
for converting strings to lowercase. It also includes a Pydantic BaseModel subclass
which applies these utilities to convert all keys in a model to lowercase.

Typical usage example:

variable = "Sample"
variable_list = ensure_list(variable)
lower_variable = to_lower("HELLO")
model_instance = BaseModelLowerCase.parse_obj({"Name": "Alice"})
"""
from __future__ import annotations

from typing import Any
from typing import TypeVar, cast

from pydantic import BaseModel, model_validator

T = TypeVar("T")


def ensure_list(variable: T | list[T] | None) -> T | list[T]:
"""Ensures that the input variable is returned as a list.

from pydantic import BaseModel, ConfigDict, model_validator
If the input variable is already a list, it is returned as-is.
If the variable is None, an empty list is returned.
Otherwise, the variable is wrapped in a list and returned.

Parameters:
variable (T | list[T] | None): The variable to be ensured as a list.

def ensure_list(variable: Any | list[Any] | None) -> list[Any]:
Returns:
T | list[T]: The input variable as a list.
"""
if isinstance(variable, list):
return variable
return cast(list[T], variable)
if variable is None:
return []
return [variable]


def to_lower(string: Any) -> Any:
def to_lower(string: T) -> T:
"""Converts a string to lowercase if it is of type str.

If the input is not a string, it is returned unchanged.

Parameters:
string (T): The string to be converted to lowercase.

Returns:
T: The lowercase string if input is a string; otherwise, the original input.
"""
if isinstance(string, str):
return string.lower()
return string


class BaseModelLowerCase(BaseModel):
model_config = ConfigDict(str_to_lower=True)
"""A Pydantic BaseModel class that converts all keys in the model to lowercase.

This is achieved using a model validator that operates before model initialization.
The validator applies the `to_lower` function to each key of the model.
"""

@model_validator(mode="before")
def key_to_lower(cls, model: dict[str, Any]) -> dict[str, Any]:
def key_to_lower(cls, model: dict[str, T]) -> dict[str, T]:
"""Model validator to convert all dictionary keys to lowercase.

Parameters:
model (dict[str, T]): The dictionary model with string keys.

Returns:
dict[str, T]: The modified dictionary with all keys in lowercase.
"""
return {to_lower(k): v for k, v in model.items()}
80 changes: 60 additions & 20 deletions chemex/configuration/conditions.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from __future__ import annotations

from functools import total_ordering
from typing import TYPE_CHECKING, Annotated, Any, Literal
from typing import TYPE_CHECKING, Annotated, Literal, TypeVar

from pydantic import (
BaseModel,
BeforeValidator,
Field,
PositiveFloat,
NonNegativeFloat,
ValidationError,
field_validator,
model_validator,
Expand All @@ -19,30 +19,46 @@
if TYPE_CHECKING:
from collections.abc import Hashable

Self = TypeVar("Self", bound="Conditions")
T = TypeVar("T")
LabelType = Annotated[Literal["1h", "2h", "13c", "15n"], BeforeValidator(to_lower)]


@total_ordering
class Conditions(BaseModel, frozen=True):
h_larmor_frq: PositiveFloat | None = None
"""Represents experimental conditions for NMR measurements.

Attributes:
h_larmor_frq (NonNegativeFloat | None): Larmor frequency of Hydrogen in MHz.
temperature (float | None): Experimental temperature in degrees Celsius.
p_total (NonNegativeFloat | None): Total concentration of protein in the sample.
l_total (NonNegativeFloat | None): Total ligand concentration in the sample.
d2o (float | None): Fraction of D2O in the solvent, between 0 and 1.
label (tuple[LabelType, ...]): Tuple of NMR active isotopes used in the experiment.
"""

h_larmor_frq: NonNegativeFloat | None = None
temperature: float | None = None
p_total: PositiveFloat | None = None
l_total: PositiveFloat | None = None
p_total: NonNegativeFloat | None = None
l_total: NonNegativeFloat | None = None
d2o: float | None = Field(gt=0.0, lt=1.0, default=None)
label: tuple[LabelType, ...] = ()

def rounded(self) -> Conditions:
def rounded(self: Self) -> Self:
"""Returns a new instance with rounded h_larmor_frq and temperature."""
h_larmor_frq = round(self.h_larmor_frq, 1) if self.h_larmor_frq else None
temperature = round(self.temperature, 1) if self.temperature else None
return self.model_copy(
update={"h_larmor_frq": h_larmor_frq, "temperature": temperature}
update={"h_larmor_frq": h_larmor_frq, "temperature": temperature},
)

def match(self, other: Conditions) -> bool:
def match(self: Self, other: Self) -> bool:
"""Checks if the current instance is equivalent to another."""
return self == self & other

@property
def search_keys(self) -> set[Hashable]:
"""Creates a set of hashable search keys based on conditions."""
return {
self.h_larmor_frq,
self.temperature,
Expand All @@ -53,6 +69,7 @@ def search_keys(self) -> set[Hashable]:

@property
def section(self) -> str:
"""Generates a string representation of the conditions."""
parts: list[str] = []
if self.temperature is not None:
parts.append(f"T->{self.temperature:.1f}C")
Expand All @@ -68,6 +85,7 @@ def section(self) -> str:

@property
def folder(self):
"""Generates a folder name representation of the conditions."""
parts: list[str] = []
if self.temperature is not None:
parts.append(f"{self.temperature:.1f}C")
Expand All @@ -83,26 +101,41 @@ def folder(self):

@property
def is_deuterated(self) -> bool:
"""Checks if the conditions include deuterium."""
return "2h" in self.label

def select_conditions(self, conditions_selection: tuple[str, ...]) -> Conditions:
return Conditions.model_construct(
def select_conditions(self: Self, conditions_selection: tuple[str, ...]) -> Self:
"""Selects specific conditions based on given keys.

Args:
conditions_selection (tuple[str, ...]): Keys to select conditions by.

Returns:
A new instance of Conditions with selected conditions.
"""
return type(self).model_construct(
**{
key: value
for key, value in self.model_dump().items()
if key in conditions_selection
}
},
)

def __and__(self, other: Conditions) -> Conditions:
def __and__(self: Self, other: object) -> Self:
if not isinstance(other, type(self)):
return NotImplemented
"""Defines bitwise AND operation for Conditions instances."""
self_dict = self.model_dump()
other_dict = other.model_dump()
intersection = {
key: value for key, value in self_dict.items() if other_dict[key] == value
}
return Conditions.model_construct(**intersection)
return type(self).model_construct(**intersection)

def __lt__(self, other: Conditions) -> bool:
def __lt__(self, other: object) -> bool:
"""Defines less than operation for Conditions instances."""
if not isinstance(other, type(self)):
return NotImplemented
tuple_self = tuple(
value if value is not None else -1e16
for value in self.model_dump().values()
Expand All @@ -116,30 +149,37 @@ def __lt__(self, other: Conditions) -> bool:

@total_ordering
class ConditionsFromFile(Conditions, frozen=True):
"""Extends Conditions to support loading experimental conditions from a file.

This class includes additional validations specific to the file-based input.
"""

@model_validator(mode="before")
def key_to_lower(cls, model: dict[str, Any]) -> dict[str, Any]:
def key_to_lower(cls, model: dict[str, T]) -> dict[str, T]:
"""Converts keys of the model dictionary to lowercase."""
return {to_lower(k): v for k, v in model.items()}

@field_validator("d2o")
def validate_d2o(cls, d2o: float | None) -> float | None:
"""Validates the d2o field for specific model requirements."""
if "hd" in model.name and d2o is None:
msg = 'To use the "hd" model, d2o must be provided'
raise ValidationError(msg)
return d2o

@field_validator("temperature")
def validate_temperature(cls, temperature: float | None) -> float | None:
"""Validates temperature field for specific model requirements."""
if "eyring" in model.name and temperature is None:
msg = 'To use the "eyring" model, "temperature" must be provided'
raise ValidationError(msg)
return temperature

@model_validator(mode="after")
def validate_p_total_l_total(
cls, conditions: ConditionsFromFile
) -> ConditionsFromFile:
are_not_both_set = conditions.p_total is None or conditions.l_total is None
def validate_p_total_l_total(self: Self) -> Self:
"""Validates both p_total and l_total for specific model requirements."""
are_not_both_set = self.p_total is None or self.l_total is None
if "binding" in model.name and are_not_both_set:
msg = 'To use the "binding" model, "p_total" and "l_total" must be provided'
raise ValidationError(msg)
return conditions
return self
7 changes: 6 additions & 1 deletion chemex/configuration/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Generic, TypeVar

import numpy as np
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

from chemex.configuration.base import BaseModelLowerCase
from chemex.configuration.conditions import ConditionsFromFile
Expand All @@ -17,14 +17,17 @@ class ToBeFitted:


class ExperimentNameSettings(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True)
name: str


class RelaxationSettings(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True)
name: str


class CpmgSettings(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True)
name: str
even_ncycs: bool = False

Expand All @@ -34,11 +37,13 @@ class CpmgSettingsEvenNcycs(CpmgSettings):


class CestSettings(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True)
name: str
sw: float = np.inf


class ShiftSettings(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True)
name: str


Expand Down
3 changes: 2 additions & 1 deletion chemex/configuration/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Literal

from pydantic import BeforeValidator, Field, ValidationError
from pydantic import BeforeValidator, ConfigDict, Field, ValidationError
from pydantic.types import PositiveInt

from chemex.configuration.base import BaseModelLowerCase
Expand Down Expand Up @@ -35,6 +35,7 @@ class Selection:


class Method(BaseModelLowerCase):
model_config = ConfigDict(str_to_lower=True, extra="forbid")
fitmethod: str = "leastsq"
include: SelectionType = None
exclude: SelectionType = None
Expand Down
Loading
Loading