Skip to content

Models

core

Modelos Pydantic para estructuras de datos núcleo de XPS.

Migración de dataclasses -> Pydantic BaseModel para: - XPSSpectrum ✓ - XPSDataset ✓ - XPSSample ✓

Proporciona validación automática avanzada para los datos fundamentales de XPS.

Classes

XPSSpectrum

Bases: XPSBaseModel

Representa un espectro XPS individual con validación automática completa.

Valida automáticamente que las energías de enlace y las intensidades sean consistentes, finitas y físicamente realistas para XPS.

Parámetros

region_name : str Nombre de la región espectral (ej: "C 1s", "survey"). No puede estar vacío. binding_energy : np.ndarray Array de energías de enlace en eV. Debe ser creciente y positivo. intensity : np.ndarray Array de intensidades en cuentas. Debe ser finito y no negativo. metadata : dict[str, Any] Metadata adicional del espectro (condiciones de medición, etc.).

Ejemplos
Espectro básico

energies = np.array([280.0, 285.0, 290.0]) intensities = np.array([100.0, 1000.0, 200.0]) spectrum = XPSSpectrum( ... region_name="C 1s", ... binding_energy=energies, ... intensity=intensities, ... metadata={"pass_energy": 20} ... )

Acceso a datos

df = spectrum.data # DataFrame con binding_energy como índice copy_spec = spectrum.copy() # Copia completa

Attributes
data property
data: DataFrame

Retorna los datos como DataFrame con binding_energy como índice.

Retorna

pd.DataFrame DataFrame con columna 'intensity' e índice 'binding_energy'.

Methods:
validate_region_name_format classmethod
validate_region_name_format(v: str) -> str

Valida formato del nombre de región.

Source code in src/xps_analyzer/models/core.py
@field_validator("region_name")
@classmethod
def validate_region_name_format(cls, v: str) -> str:
    """Valida formato del nombre de región."""
    return XPSValidators.validate_region_name(v)
validate_binding_energy_array classmethod
validate_binding_energy_array(v: ndarray) -> np.ndarray

Valida array de energías de enlace.

Source code in src/xps_analyzer/models/core.py
@field_validator("binding_energy")
@classmethod
def validate_binding_energy_array(cls, v: np.ndarray) -> np.ndarray:
    """Valida array de energías de enlace."""
    validated = XPSValidators.validate_binding_energy(v)

    # Validaciones adicionales específicas para XPS
    if len(validated) < 2:
        raise ValueError("binding_energy debe tener al menos 2 puntos")

    # Verificar que esté aproximadamente ordenado (tolerancia para ruido)
    diffs = np.diff(validated)
    if np.sum(diffs < 0) > len(diffs) * 0.1:  # Máximo 10% decrementos
        raise ValueError(
            "binding_energy debe estar aproximadamente en orden creciente"
        )

    return validated
validate_intensity_array classmethod
validate_intensity_array(v: ndarray) -> np.ndarray

Valida array de intensidades.

Source code in src/xps_analyzer/models/core.py
@field_validator("intensity")
@classmethod
def validate_intensity_array(cls, v: np.ndarray) -> np.ndarray:
    """Valida array de intensidades."""
    return XPSValidators.validate_intensity(v)
validate_array_consistency
validate_array_consistency() -> XPSSpectrum

Valida consistencia entre arrays.

Source code in src/xps_analyzer/models/core.py
@model_validator(mode="after")
def validate_array_consistency(self) -> XPSSpectrum:
    """Valida consistencia entre arrays."""
    NumpyArrayValidator.validate_matching_lengths(
        self.binding_energy, self.intensity, "binding_energy", "intensity"
    )
    return self
validate_realistic_data_range
validate_realistic_data_range() -> XPSSpectrum

Valida que los datos estén en rangos físicamente realistas.

Source code in src/xps_analyzer/models/core.py
@model_validator(mode="after")
def validate_realistic_data_range(self) -> XPSSpectrum:
    """Valida que los datos estén en rangos físicamente realistas."""
    # Energías de enlace típicas en XPS: 0-2000 eV
    if self.binding_energy.max() > 2000:
        raise ValueError(
            f"binding_energy máxima ({self.binding_energy.max():.1f}) "
            f"excede rango típico de XPS (0-2000 eV)"
        )

    # Rango de energía debe ser razonable (mínimo 1 eV)
    energy_range = self.binding_energy.max() - self.binding_energy.min()
    if energy_range < 1.0:
        raise ValueError(
            f"Rango de energía ({energy_range:.2f} eV) muy pequeño. "
            f"Mínimo 1.0 eV requerido"
        )

    # Intensidades muy altas pueden indicar problemas
    if self.intensity.max() > 1e6:
        # Advertencia, no error
        pass

    return self
copy
copy() -> XPSSpectrum

Retorna una copia completa del espectro.

Retorna

XPSSpectrum Copia independiente del espectro.

Source code in src/xps_analyzer/models/core.py
def copy(self) -> XPSSpectrum:
    """
    Retorna una copia completa del espectro.

    Retorna
    -------
    XPSSpectrum
        Copia independiente del espectro.
    """
    return XPSSpectrum(
        region_name=self.region_name,
        binding_energy=self.binding_energy.copy(),
        intensity=self.intensity.copy(),
        metadata=self.metadata.copy(),
    )
get_energy_range
get_energy_range() -> tuple[float, float]

Obtiene el rango de energías de enlace.

Retorna

tuple[float, float] (energía_mínima, energía_máxima) en eV.

Source code in src/xps_analyzer/models/core.py
def get_energy_range(self) -> tuple[float, float]:
    """
    Obtiene el rango de energías de enlace.

    Retorna
    -------
    tuple[float, float]
        (energía_mínima, energía_máxima) en eV.
    """
    return (float(self.binding_energy.min()), float(self.binding_energy.max()))
get_intensity_stats
get_intensity_stats() -> dict[str, float]

Obtiene estadísticas básicas de intensidad.

Retorna

dict[str, float] Diccionario con estadísticas: max, min, mean, std.

Source code in src/xps_analyzer/models/core.py
def get_intensity_stats(self) -> dict[str, float]:
    """
    Obtiene estadísticas básicas de intensidad.

    Retorna
    -------
    dict[str, float]
        Diccionario con estadísticas: max, min, mean, std.
    """
    return {
        "max": float(self.intensity.max()),
        "min": float(self.intensity.min()),
        "mean": float(self.intensity.mean()),
        "std": float(self.intensity.std()),
    }

XPSDataset

Bases: XPSBaseModel

Representa un archivo XPS completo con múltiples espectros.

Valida automáticamente que el dataset contenga espectros válidos, nombres de archivo apropiados y metadata consistente.

Parámetros

filename : str Nombre del archivo. Debe ser no vacío y válido. header : dict[str, Any] Metadata del archivo (condiciones instrumentales, fecha, etc.). spectra : dict[str, XPSSpectrum] Diccionario de espectros indexados por nombre de región.

Ejemplos
Dataset básico

spectrum = XPSSpectrum(...) dataset = XPSDataset( ... filename="sample1.txt", ... header={"date": "2024-03-15", "operator": "researcher"}, ... spectra={"C 1s": spectrum} ... )

Acceso a espectros

c1s = dataset.get_spectrum("C 1s") regions = dataset.list_regions()

Methods:
validate_filename_format classmethod
validate_filename_format(v: str) -> str

Valida formato básico del filename.

Source code in src/xps_analyzer/models/core.py
@field_validator("filename")
@classmethod
def validate_filename_format(cls, v: str) -> str:
    """Valida formato básico del filename."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("filename no puede estar vacío")

    # Validación básica de caracteres
    invalid_chars = ["<", ">", ":", '"', "|", "?", "*"]
    if any(char in cleaned for char in invalid_chars):
        raise ValueError(f"filename contiene caracteres inválidos: {invalid_chars}")

    return cleaned
validate_spectra_consistency
validate_spectra_consistency() -> XPSDataset

Valida consistencia entre espectros y claves del diccionario.

Source code in src/xps_analyzer/models/core.py
@model_validator(mode="after")
def validate_spectra_consistency(self) -> XPSDataset:
    """Valida consistencia entre espectros y claves del diccionario."""
    for region_name, spectrum in self.spectra.items():
        if region_name != spectrum.region_name:
            raise ValueError(
                f"Inconsistencia: clave '{region_name}' no coincide "
                f"con spectrum.region_name '{spectrum.region_name}'"
            )
    return self
get_spectrum
get_spectrum(region_name: str) -> XPSSpectrum | None

Obtiene un espectro específico por nombre de región.

Parámetros

region_name : str Nombre de la región a buscar.

Retorna

XPSSpectrum | None Espectro encontrado o None si no existe.

Source code in src/xps_analyzer/models/core.py
def get_spectrum(self, region_name: str) -> XPSSpectrum | None:
    """
    Obtiene un espectro específico por nombre de región.

    Parámetros
    ----------
    region_name : str
        Nombre de la región a buscar.

    Retorna
    -------
    XPSSpectrum | None
        Espectro encontrado o None si no existe.
    """
    return self.spectra.get(region_name)
list_regions
list_regions() -> list[str]

Lista todas las regiones espectrales disponibles.

Retorna

list[str] Lista de nombres de regiones ordenada alfabéticamente.

Source code in src/xps_analyzer/models/core.py
def list_regions(self) -> list[str]:
    """
    Lista todas las regiones espectrales disponibles.

    Retorna
    -------
    list[str]
        Lista de nombres de regiones ordenada alfabéticamente.
    """
    return sorted(self.spectra.keys())
copy
copy() -> XPSDataset

Retorna una copia completa del dataset.

Retorna

XPSDataset Copia independiente del dataset.

Source code in src/xps_analyzer/models/core.py
def copy(self) -> XPSDataset:
    """
    Retorna una copia completa del dataset.

    Retorna
    -------
    XPSDataset
        Copia independiente del dataset.
    """
    return XPSDataset(
        filename=self.filename,
        header=self.header.copy(),
        spectra={name: spec.copy() for name, spec in self.spectra.items()},
    )
get_statistics
get_statistics() -> dict[str, Any]

Obtiene estadísticas del dataset completo.

Retorna

dict[str, Any] Diccionario con estadísticas: número de espectros, rangos, etc.

Source code in src/xps_analyzer/models/core.py
def get_statistics(self) -> dict[str, Any]:
    """
    Obtiene estadísticas del dataset completo.

    Retorna
    -------
    dict[str, Any]
        Diccionario con estadísticas: número de espectros, rangos, etc.
    """
    total_points = sum(len(spec.binding_energy) for spec in self.spectra.values())

    energy_ranges = [spec.get_energy_range() for spec in self.spectra.values()]
    global_min = min(er[0] for er in energy_ranges)
    global_max = max(er[1] for er in energy_ranges)

    return {
        "total_spectra": len(self.spectra),
        "total_data_points": total_points,
        "energy_range": (global_min, global_max),
        "regions": list(self.spectra.keys()),
        "filename": self.filename,
    }

XPSSample

Bases: XPSBaseModel

Representa una muestra XPS completa con múltiples archivos/datasets.

Valida automáticamente que la muestra tenga datasets válidos y nombres apropiados, y proporciona acceso unificado a todos los datos.

Parámetros

sample_name : str Nombre identificador de la muestra. Debe ser no vacío. datasets : dict[str, XPSDataset] Diccionario de datasets indexados por filename.

Ejemplos
Muestra con múltiples archivos

survey_dataset = XPSDataset(...) multiplex_dataset = XPSDataset(...) sample = XPSSample( ... sample_name="Sample_001", ... datasets={ ... "survey.txt": survey_dataset, ... "multiplex.txt": multiplex_dataset ... } ... )

Acceso a datos

survey = sample.get_dataset("survey.txt") all_spectra = sample.get_all_spectra()

Methods:
validate_sample_name_format classmethod
validate_sample_name_format(v: str) -> str

Valida formato del nombre de muestra.

Source code in src/xps_analyzer/models/core.py
@field_validator("sample_name")
@classmethod
def validate_sample_name_format(cls, v: str) -> str:
    """Valida formato del nombre de muestra."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("sample_name no puede estar vacío")
    return cleaned
validate_datasets_consistency
validate_datasets_consistency() -> XPSSample

Valida consistencia entre datasets y claves del diccionario.

Source code in src/xps_analyzer/models/core.py
@model_validator(mode="after")
def validate_datasets_consistency(self) -> XPSSample:
    """Valida consistencia entre datasets y claves del diccionario."""
    for filename, dataset in self.datasets.items():
        if filename != dataset.filename:
            raise ValueError(
                f"Inconsistencia: clave '{filename}' no coincide "
                f"con dataset.filename '{dataset.filename}'"
            )
    return self
get_dataset
get_dataset(filename: str) -> XPSDataset | None

Obtiene un dataset específico por filename.

Parámetros

filename : str Nombre del archivo del dataset.

Retorna

XPSDataset | None Dataset encontrado o None si no existe.

Source code in src/xps_analyzer/models/core.py
def get_dataset(self, filename: str) -> XPSDataset | None:
    """
    Obtiene un dataset específico por filename.

    Parámetros
    ----------
    filename : str
        Nombre del archivo del dataset.

    Retorna
    -------
    XPSDataset | None
        Dataset encontrado o None si no existe.
    """
    return self.datasets.get(filename)
list_datasets
list_datasets() -> list[str]

Lista todos los filenames de datasets disponibles.

Retorna

list[str] Lista de filenames ordenada alfabéticamente.

Source code in src/xps_analyzer/models/core.py
def list_datasets(self) -> list[str]:
    """
    Lista todos los filenames de datasets disponibles.

    Retorna
    -------
    list[str]
        Lista de filenames ordenada alfabéticamente.
    """
    return sorted(self.datasets.keys())
get_all_spectra
get_all_spectra() -> dict[str, dict[str, XPSSpectrum]]

Obtiene todos los espectros organizados por dataset y región.

Retorna

dict[str, dict[str, XPSSpectrum]] Diccionario anidado: {filename: {region: spectrum}}.

Source code in src/xps_analyzer/models/core.py
def get_all_spectra(self) -> dict[str, dict[str, XPSSpectrum]]:
    """
    Obtiene todos los espectros organizados por dataset y región.

    Retorna
    -------
    dict[str, dict[str, XPSSpectrum]]
        Diccionario anidado: {filename: {region: spectrum}}.
    """
    return {
        filename: dataset.spectra for filename, dataset in self.datasets.items()
    }
find_spectra_by_region
find_spectra_by_region(region_name: str) -> dict[str, XPSSpectrum]

Busca espectros de una región específica en todos los datasets.

Parámetros

region_name : str Nombre de la región a buscar.

Retorna

dict[str, XPSSpectrum] Diccionario {filename: spectrum} para la región especificada.

Source code in src/xps_analyzer/models/core.py
def find_spectra_by_region(self, region_name: str) -> dict[str, XPSSpectrum]:
    """
    Busca espectros de una región específica en todos los datasets.

    Parámetros
    ----------
    region_name : str
        Nombre de la región a buscar.

    Retorna
    -------
    dict[str, XPSSpectrum]
        Diccionario {filename: spectrum} para la región especificada.
    """
    result = {}
    for filename, dataset in self.datasets.items():
        spectrum = dataset.get_spectrum(region_name)
        if spectrum is not None:
            result[filename] = spectrum
    return result
get_sample_statistics
get_sample_statistics() -> dict[str, Any]

Obtiene estadísticas completas de la muestra.

Retorna

dict[str, Any] Diccionario con estadísticas agregadas de todos los datasets.

Source code in src/xps_analyzer/models/core.py
def get_sample_statistics(self) -> dict[str, Any]:
    """
    Obtiene estadísticas completas de la muestra.

    Retorna
    -------
    dict[str, Any]
        Diccionario con estadísticas agregadas de todos los datasets.
    """
    total_datasets = len(self.datasets)
    total_spectra = sum(len(ds.spectra) for ds in self.datasets.values())
    total_points = sum(
        sum(len(spec.binding_energy) for spec in ds.spectra.values())
        for ds in self.datasets.values()
    )

    # Recopilar todas las regiones únicas
    all_regions = set()
    for dataset in self.datasets.values():
        all_regions.update(dataset.spectra.keys())

    return {
        "sample_name": self.sample_name,
        "total_datasets": total_datasets,
        "total_spectra": total_spectra,
        "total_data_points": total_points,
        "unique_regions": sorted(all_regions),
        "dataset_filenames": list(self.datasets.keys()),
    }

analysis

Modelos Pydantic para resultados de análisis XPS.

Migración de dataclasses -> Pydantic BaseModel para: - PeakParameters ✓ - FitResult ✓ - BackgroundResult (futuro)

Proporciona validación automática para parámetros de ajuste y resultados de análisis.

Classes

PeakParameters

Bases: XPSBaseModel

Parámetros de un pico ajustado con validación automática.

Valida automáticamente que los parámetros físicos sean realistas y consistentes entre sí para espectroscopía XPS.

Parámetros

position : float Posición del pico (binding energy en eV). Debe ser positiva. amplitude : float Amplitud del pico (intensidad máxima en cuentas). Debe ser positiva. width : float Ancho del pico (FWHM en eV para gaussiano/lorentziano, sigma para Voigt). Debe ser positiva y realista para XPS (< 10 eV). area : float Área integrada bajo el pico. Debe ser positiva. shape : Literal["gaussian", "lorentzian", "voigt", "pseudo_voigt", "gl"] Tipo de perfil del pico. gamma : float, optional Parámetro gamma para perfil Voigt (ancho lorentziano). Solo requerido para Voigt. position_error : float, optional Error estándar en la posición del pico. Debe ser no negativo. amplitude_error : float, optional Error estándar en la amplitud. Debe ser no negativo. width_error : float, optional Error estándar en el ancho. Debe ser no negativo.

Ejemplos
Pico gaussiano básico

peak = PeakParameters( ... position=284.8, ... amplitude=1000.0, ... width=1.2, ... area=1500.0, ... shape="gaussian" ... )

Pico Voigt con errores

voigt_peak = PeakParameters( ... position=531.1, ... amplitude=800.0, ... width=1.8, ... area=2000.0, ... shape="voigt", ... gamma=0.5, ... position_error=0.1, ... amplitude_error=50.0 ... )

Methods:
validate_voigt_gamma
validate_voigt_gamma() -> PeakParameters

Valida que picos Voigt tengan parámetro gamma.

Source code in src/xps_analyzer/models/analysis.py
@model_validator(mode="after")
def validate_voigt_gamma(self) -> PeakParameters:
    """Valida que picos Voigt tengan parámetro gamma."""
    if self.shape == "voigt" and self.gamma is None:
        raise ValueError("Perfil Voigt requiere parámetro gamma")
    return self
validate_area_consistency
validate_area_consistency() -> PeakParameters

Valida consistencia entre área, amplitud y ancho.

Source code in src/xps_analyzer/models/analysis.py
@model_validator(mode="after")
def validate_area_consistency(self) -> PeakParameters:
    """Valida consistencia entre área, amplitud y ancho."""
    amplitude = self.amplitude
    width = self.width
    area = self.area

    # Área mínima esperada para gaussian ~ amplitude * width * sqrt(2*pi) / 2
    min_expected_area = amplitude * width * 0.5
    max_expected_area = amplitude * width * 5.0

    if not (min_expected_area <= area <= max_expected_area):
        raise ValueError(
            f"Área ({area:.1f}) inconsistente con amplitud ({amplitude:.1f}) "
            f"y ancho ({width:.1f}). Esperada entre "
            f"{min_expected_area:.1f} y {max_expected_area:.1f}"
        )

    return self

FitResult

Bases: XPSBaseModel

Resultado de un ajuste de pico(s) con validación automática.

Valida automáticamente que los resultados estadísticos sean consistentes y que los arrays tengan dimensiones correctas.

Parámetros

peaks : list[PeakParameters] Lista de parámetros de picos ajustados. No puede estar vacía. fitted_spectrum : np.ndarray Espectro ajustado (suma de todos los picos). Debe ser finito. residual : np.ndarray Residual (espectro original - ajuste). Misma longitud que fitted_spectrum. r_squared : float Coeficiente de determinación R² (bondad de ajuste). Debe estar en [0, 1]. chi_squared : float Chi-cuadrado reducido. Debe ser positivo. success : bool Si el ajuste convergió exitosamente. message : str Mensaje sobre el resultado del ajuste. No puede estar vacío.

Ejemplos
Ajuste exitoso con un pico

result = FitResult( ... peaks=[peak_params], ... fitted_spectrum=np.array([100, 200, 300]), ... residual=np.array([5, -2, 1]), ... r_squared=0.95, ... chi_squared=1.2, ... success=True, ... message="Ajuste convergió exitosamente" ... )

Methods:
validate_fitted_spectrum classmethod
validate_fitted_spectrum(v: ndarray) -> np.ndarray

Valida que el espectro ajustado sea finito y no negativo.

Source code in src/xps_analyzer/models/analysis.py
@field_validator("fitted_spectrum")
@classmethod
def validate_fitted_spectrum(cls, v: np.ndarray) -> np.ndarray:
    """Valida que el espectro ajustado sea finito y no negativo."""
    return NumpyArrayValidator.validate_finite_array(v, "fitted_spectrum")
validate_residual classmethod
validate_residual(v: ndarray) -> np.ndarray

Valida que el residual sea finito.

Source code in src/xps_analyzer/models/analysis.py
@field_validator("residual")
@classmethod
def validate_residual(cls, v: np.ndarray) -> np.ndarray:
    """Valida que el residual sea finito."""
    return NumpyArrayValidator.validate_finite_array(v, "residual")
validate_array_lengths
validate_array_lengths() -> FitResult

Valida que fitted_spectrum y residual tengan la misma longitud.

Source code in src/xps_analyzer/models/analysis.py
@model_validator(mode="after")
def validate_array_lengths(self) -> FitResult:
    """Valida que fitted_spectrum y residual tengan la misma longitud."""
    NumpyArrayValidator.validate_matching_lengths(
        self.fitted_spectrum, self.residual, "fitted_spectrum", "residual"
    )
    return self
validate_r_squared_realism classmethod
validate_r_squared_realism(v: float) -> float

Valida que R² sea realista para XPS.

Source code in src/xps_analyzer/models/analysis.py
@field_validator("r_squared")
@classmethod
def validate_r_squared_realism(cls, v: float) -> float:
    """Valida que R² sea realista para XPS."""
    if v < 0.5:
        # Advertencia pero no error - ajustes pobres pueden ser informativos
        pass
    return v
validate_chi_squared_realism classmethod
validate_chi_squared_realism(v: float) -> float

Valida que chi² sea realista.

Source code in src/xps_analyzer/models/analysis.py
@field_validator("chi_squared")
@classmethod
def validate_chi_squared_realism(cls, v: float) -> float:
    """Valida que chi² sea realista."""
    if v > 10.0:
        # Advertencia: chi² muy alto sugiere mal ajuste
        pass
    return v

reference

Modelos Pydantic para datos de referencia XPS.

Migración de dataclasses -> Pydantic BaseModel para: - PhotoelectronLine ✓ - CompoundReference ✓ - ElementReference ✓ - ReferenceDatabase ✓

Proporciona validación automática y serialización mejorada para datos de referencia.

Classes

PhotoelectronLine

Bases: XPSBaseModel

Representa una línea fotoeléctrica de un orbital específico.

Esta clase valida automáticamente energías de enlace positivas, fuentes de rayos X válidas y tipos de línea correctos.

Parámetros

line : str Designación de la línea (ej: "1s", "2p1/2", "2p3/2", "KLL"). Debe ser no vacía. binding_energy : float Energía de enlace en eV. Debe ser positiva. x_ray_source : str, optional Fuente de rayos X utilizada (ej: "Mg_Ka", "Al_Ka"). type : Literal["core", "Auger"], default="core" Tipo de línea, debe ser "core" o "Auger". kinetic_energy : float, optional Energía cinética en eV (solo para líneas Auger). Debe ser positiva si se especifica.

Ejemplos
Línea core básica

line = PhotoelectronLine( ... line="1s", ... binding_energy=284.8 ... )

Línea Auger completa

auger_line = PhotoelectronLine( ... line="KLL", ... binding_energy=1200.0, ... x_ray_source="Al_Ka", ... type="Auger", ... kinetic_energy=267.0 ... )

Methods:
validate_line_format classmethod
validate_line_format(v: str) -> str

Valida formato de designación orbital.

Source code in src/xps_analyzer/models/reference.py
@field_validator("line")
@classmethod
def validate_line_format(cls, v: str) -> str:
    """Valida formato de designación orbital."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("line no puede estar vacía")
    return cleaned
validate_auger_kinetic_energy
validate_auger_kinetic_energy() -> PhotoelectronLine

Valida que líneas Auger tengan energía cinética.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_auger_kinetic_energy(self) -> PhotoelectronLine:
    """Valida que líneas Auger tengan energía cinética."""
    if self.type == "Auger" and self.kinetic_energy is None:
        raise ValueError("Líneas Auger requieren kinetic_energy")
    return self

CompoundReference

Bases: XPSBaseModel

Datos de referencia para un compuesto específico.

Valida automáticamente rangos de energía de enlace consistentes y posiciones de pico dentro del rango especificado.

Parámetros

orbital : str Orbital asociado (ej: "1s", "2p"). Debe ser no vacío. binding_energy_range : tuple[float, float] Rango de energías de enlace (min, max) en eV. min < max, ambos > 0. peak_position : float, optional Posición del pico principal en eV. Debe estar dentro del rango. chemical_shift : float, optional Desplazamiento químico respecto al elemento puro en eV.

Ejemplos
Compuesto básico

compound = CompoundReference( ... orbital="1s", ... binding_energy_range=(284.0, 289.0), ... peak_position=286.5 ... )

Con desplazamiento químico

oxide = CompoundReference( ... orbital="1s", ... binding_energy_range=(531.0, 534.0), ... peak_position=532.1, ... chemical_shift=2.1 ... )

Methods:
validate_energy_range classmethod
validate_energy_range(v: tuple[float, float]) -> tuple[float, float]

Valida que el rango de energía sea consistente.

Source code in src/xps_analyzer/models/reference.py
@field_validator("binding_energy_range")
@classmethod
def validate_energy_range(cls, v: tuple[float, float]) -> tuple[float, float]:
    """Valida que el rango de energía sea consistente."""
    min_energy, max_energy = v

    if min_energy <= 0 or max_energy <= 0:
        raise ValueError("Las energías de enlace deben ser positivas")

    if min_energy >= max_energy:
        raise ValueError(
            f"min_energy ({min_energy}) debe ser menor que max_energy ({max_energy})"
        )

    if max_energy - min_energy > 50:  # Rango muy amplio es sospechoso
        raise ValueError(
            f"Rango de energías muy amplio: {max_energy - min_energy:.1f} eV"
        )

    return v
validate_peak_in_range
validate_peak_in_range() -> CompoundReference

Valida que la posición del pico esté dentro del rango.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_peak_in_range(self) -> CompoundReference:
    """Valida que la posición del pico esté dentro del rango."""
    if self.peak_position is not None:
        min_energy, max_energy = self.binding_energy_range
        if not (min_energy <= self.peak_position <= max_energy):
            raise ValueError(
                f"peak_position ({self.peak_position}) debe estar en el rango "
                f"[{min_energy}, {max_energy}]"
            )
    return self
validate_orbital_format classmethod
validate_orbital_format(v: str) -> str

Valida formato básico de orbital.

Source code in src/xps_analyzer/models/reference.py
@field_validator("orbital")
@classmethod
def validate_orbital_format(cls, v: str) -> str:
    """Valida formato básico de orbital."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("orbital no puede estar vacío")
    return cleaned

ElementReference

Bases: XPSBaseModel

Base de datos completa de un elemento químico con validación automática.

Valida automáticamente símbolos de elemento, números atómicos, energías de enlace y consistencia entre líneas fotoeléctronicas.

Parámetros

symbol : str Símbolo del elemento (ej: "Li", "C", "O"). Debe tener 1-2 caracteres. element : str Nombre completo del elemento. Debe ser no vacío. atomic_number : int Número atómico. Debe ser positivo (1-118). photoelectron_lines : list[PhotoelectronLine] Lista con las líneas fotoeléctronicas por orbital. No puede estar vacía. compounds : dict[str, CompoundReference] Diccionario de compuestos de referencia por nombre. binding_energy_most_useful : float, optional Energía de enlace más útil en eV. Debe ser positiva. spin_orbital_splitting : float, optional Separación spin-orbital en eV. Debe ser positiva.

Ejemplos
Elemento básico con líneas core

carbon = ElementReference( ... symbol="C", ... element="Carbon", ... atomic_number=6, ... photoelectron_lines=[ ... PhotoelectronLine(line="1s", binding_energy=284.8) ... ], ... compounds={} ... )

Elemento con compuestos

oxygen = ElementReference( ... symbol="O", ... element="Oxygen", ... atomic_number=8, ... photoelectron_lines=[...], ... compounds={ ... "oxide": CompoundReference( ... orbital="1s", ... binding_energy_range=(531.0, 533.0) ... ) ... }, ... binding_energy_most_useful=531.0 ... )

Methods:
validate_symbol_format classmethod
validate_symbol_format(v: str) -> str

Valida formato de símbolo químico.

Source code in src/xps_analyzer/models/reference.py
@field_validator("symbol")
@classmethod
def validate_symbol_format(cls, v: str) -> str:
    """Valida formato de símbolo químico."""
    return XPSValidators.validate_element_symbol(v)
validate_element_name classmethod
validate_element_name(v: str) -> str

Valida nombre del elemento.

Source code in src/xps_analyzer/models/reference.py
@field_validator("element")
@classmethod
def validate_element_name(cls, v: str) -> str:
    """Valida nombre del elemento."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("element no puede estar vacío")
    return cleaned.title()  # Primera letra mayúscula
validate_symbol_atomic_number_consistency
validate_symbol_atomic_number_consistency() -> ElementReference

Valida consistencia básica entre símbolo y número atómico.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_symbol_atomic_number_consistency(self) -> ElementReference:
    """Valida consistencia básica entre símbolo y número atómico."""
    # Validación básica de elementos comunes en XPS
    common_elements = {
        "H": 1,
        "C": 6,
        "N": 7,
        "O": 8,
        "F": 9,
        "Na": 11,
        "Mg": 12,
        "Al": 13,
        "Si": 14,
        "P": 15,
        "S": 16,
        "Cl": 17,
        "K": 19,
        "Ca": 20,
        "Ti": 22,
        "V": 23,
        "Cr": 24,
        "Mn": 25,
        "Fe": 26,
        "Co": 27,
        "Ni": 28,
        "Cu": 29,
        "Zn": 30,
        "Ga": 31,
        "Ge": 32,
        "As": 33,
        "Se": 34,
        "Br": 35,
        "Sr": 38,
        "Zr": 40,
        "Mo": 42,
        "Ag": 47,
        "Cd": 48,
        "In": 49,
        "Sn": 50,
        "I": 53,
        "Ba": 56,
        "Au": 79,
        "Pb": 82,
        "Bi": 83,
    }

    if self.symbol in common_elements:
        expected_z = common_elements[self.symbol]
        if self.atomic_number != expected_z:
            raise ValueError(
                f"Número atómico inconsistente: {self.symbol} debe tener Z={expected_z}, "
                f"encontrado Z={self.atomic_number}"
            )

    return self
validate_most_useful_energy_exists
validate_most_useful_energy_exists() -> ElementReference

Valida que la energía más útil corresponda a una línea existente.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_most_useful_energy_exists(self) -> ElementReference:
    """Valida que la energía más útil corresponda a una línea existente."""
    if self.binding_energy_most_useful is not None:
        # Buscar si existe una línea con energía similar (±1 eV)
        found_matching_line = False
        tolerance = 1.0

        for line in self.photoelectron_lines:
            if (
                abs(line.binding_energy - self.binding_energy_most_useful)
                <= tolerance
            ):
                found_matching_line = True
                break

        if not found_matching_line:
            raise ValueError(
                f"binding_energy_most_useful ({self.binding_energy_most_useful:.1f} eV) "
                f"no corresponde a ninguna línea existente (±{tolerance} eV)"
            )

    return self
get_main_line
get_main_line() -> PhotoelectronLine

Obtiene la línea fotoeléctrica principal (mayor intensidad o más útil).

Si se especifica binding_energy_most_useful, retorna la línea más cercana. De lo contrario, retorna la primera línea disponible.

Retorna

PhotoelectronLine Línea fotoeléctrica principal.

Levanta

ValueError Si no hay líneas disponibles.

Source code in src/xps_analyzer/models/reference.py
def get_main_line(self) -> PhotoelectronLine:
    """
    Obtiene la línea fotoeléctrica principal (mayor intensidad o más útil).

    Si se especifica binding_energy_most_useful, retorna la línea más cercana.
    De lo contrario, retorna la primera línea disponible.

    Retorna
    -------
    PhotoelectronLine
        Línea fotoeléctrica principal.

    Levanta
    ------
    ValueError
        Si no hay líneas disponibles.
    """
    if not self.photoelectron_lines:
        raise ValueError(
            f"No hay líneas fotoeléctronicas disponibles para {self.symbol}"
        )

    if self.binding_energy_most_useful is not None:
        # Buscar línea más cercana a la energía más útil
        closest_line = min(
            self.photoelectron_lines,
            key=lambda line: abs(
                line.binding_energy - self.binding_energy_most_useful
            ),
        )
        return closest_line
    else:
        # Retornar primera línea disponible
        return self.photoelectron_lines[0]
get_line_by_orbital
get_line_by_orbital(orbital: str) -> PhotoelectronLine | None

Busca una línea específica por nombre de orbital.

Parámetros

orbital : str Nombre del orbital (ej: "1s", "2p").

Retorna

PhotoelectronLine | None Línea encontrada o None si no existe.

Source code in src/xps_analyzer/models/reference.py
def get_line_by_orbital(self, orbital: str) -> PhotoelectronLine | None:
    """
    Busca una línea específica por nombre de orbital.

    Parámetros
    ----------
    orbital : str
        Nombre del orbital (ej: "1s", "2p").

    Retorna
    -------
    PhotoelectronLine | None
        Línea encontrada o None si no existe.
    """
    for line in self.photoelectron_lines:
        if line.line.lower() == orbital.lower():
            return line
    return None

ReferenceDatabase

Bases: XPSBaseModel

Base de datos completa de elementos de referencia con validación automática.

Valida automáticamente la integridad de la base de datos, símbolos únicos y consistencia de versiones.

Parámetros

elements : dict[str, ElementReference] Diccionario con elementos indexados por símbolo. Símbolos deben ser únicos. version : str Versión de la base de datos. Formato recomendado: "X.Y" o "X.Y.Z". source : str Fuente de los datos de referencia. Debe ser no vacía.

Ejemplos
Base de datos básica

carbon = ElementReference( ... symbol="C", element="Carbon", atomic_number=6, ... photoelectron_lines=[PhotoelectronLine(line="1s", binding_energy=284.8)], ... compounds={} ... ) db = ReferenceDatabase( ... elements={"C": carbon}, ... version="1.0", ... source="NIST XPS Database" ... )

Methods:
validate_version_format classmethod
validate_version_format(v: str) -> str

Valida formato básico de versión.

Source code in src/xps_analyzer/models/reference.py
@field_validator("version")
@classmethod
def validate_version_format(cls, v: str) -> str:
    """Valida formato básico de versión."""
    cleaned = v.strip()
    if not cleaned:
        raise ValueError("version no puede estar vacía")

    # Formato básico: dígitos y puntos
    import re

    if not re.match(r"^[\d\.]+$", cleaned):
        # Permitir también formatos como "2024.03"
        if not re.match(r"^[\d\.]+[\w]*$", cleaned):
            raise ValueError(
                f"Formato de versión inválido: '{cleaned}'. Use formato X.Y o X.Y.Z"
            )

    return cleaned
validate_element_symbol_consistency
validate_element_symbol_consistency() -> ReferenceDatabase

Valida que las claves coincidan con los símbolos de elementos.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_element_symbol_consistency(self) -> ReferenceDatabase:
    """Valida que las claves coincidan con los símbolos de elementos."""
    for symbol, element in self.elements.items():
        if symbol != element.symbol:
            raise ValueError(
                f"Inconsistencia de símbolo: clave '{symbol}' no coincide "
                f"con element.symbol '{element.symbol}'"
            )
    return self
validate_unique_atomic_numbers
validate_unique_atomic_numbers() -> ReferenceDatabase

Valida que no haya números atómicos duplicados.

Source code in src/xps_analyzer/models/reference.py
@model_validator(mode="after")
def validate_unique_atomic_numbers(self) -> ReferenceDatabase:
    """Valida que no haya números atómicos duplicados."""
    atomic_numbers = {}
    for symbol, element in self.elements.items():
        z = element.atomic_number
        if z in atomic_numbers:
            raise ValueError(
                f"Número atómico duplicado: Z={z} para elementos "
                f"'{atomic_numbers[z]}' y '{symbol}'"
            )
        atomic_numbers[z] = symbol
    return self
get_element
get_element(symbol: str) -> ElementReference | None

Obtiene datos de un elemento por símbolo.

Parámetros

symbol : str Símbolo del elemento (case-insensitive).

Retorna

ElementReference | None Referencia del elemento si existe, None de lo contrario.

Source code in src/xps_analyzer/models/reference.py
def get_element(self, symbol: str) -> ElementReference | None:
    """
    Obtiene datos de un elemento por símbolo.

    Parámetros
    ----------
    symbol : str
        Símbolo del elemento (case-insensitive).

    Retorna
    -------
    ElementReference | None
        Referencia del elemento si existe, None de lo contrario.
    """
    return self.elements.get(symbol.upper())
search_by_binding_energy
search_by_binding_energy(
    energy: float, tolerance: float = 2.0
) -> list[tuple[str, str]]

Busca elementos/compuestos por energía de enlace.

Parámetros

energy : float Energía de enlace a buscar en eV. tolerance : float, default=2.0 Tolerancia de búsqueda en eV.

Retorna

list[tuple[str, str]] Lista de tuplas (símbolo_elemento, orbital/compuesto).

Ejemplos

db.search_by_binding_energy(284.8, tolerance=1.0) [('C', '1s'), ('C', 'graphite')]

Source code in src/xps_analyzer/models/reference.py
def search_by_binding_energy(
    self, energy: float, tolerance: float = 2.0
) -> list[tuple[str, str]]:
    """
    Busca elementos/compuestos por energía de enlace.

    Parámetros
    ----------
    energy : float
        Energía de enlace a buscar en eV.
    tolerance : float, default=2.0
        Tolerancia de búsqueda en eV.

    Retorna
    -------
    list[tuple[str, str]]
        Lista de tuplas (símbolo_elemento, orbital/compuesto).

    Ejemplos
    --------
    >>> db.search_by_binding_energy(284.8, tolerance=1.0)
    [('C', '1s'), ('C', 'graphite')]
    """
    if energy <= 0:
        raise ValueError("energy debe ser positiva")
    if tolerance <= 0:
        raise ValueError("tolerance debe ser positiva")

    matches = []

    for element in self.elements.values():
        # Buscar en líneas fotoeléctronicas
        for line in element.photoelectron_lines:
            if abs(line.binding_energy - energy) <= tolerance:
                matches.append((element.symbol, line.line))

        # Buscar en compuestos con peak_position definido
        for compound_name, compound in element.compounds.items():
            if (
                compound.peak_position is not None
                and abs(compound.peak_position - energy) <= tolerance
            ):
                matches.append((element.symbol, compound_name))

    return matches
get_chemical_shifts
get_chemical_shifts(element_symbol: str) -> dict[str, float]

Obtiene todos los desplazamientos químicos de un elemento.

Parámetros

element_symbol : str Símbolo del elemento.

Retorna

dict[str, float] Diccionario con compuesto -> desplazamiento químico.

Source code in src/xps_analyzer/models/reference.py
def get_chemical_shifts(self, element_symbol: str) -> dict[str, float]:
    """
    Obtiene todos los desplazamientos químicos de un elemento.

    Parámetros
    ----------
    element_symbol : str
        Símbolo del elemento.

    Retorna
    -------
    dict[str, float]
        Diccionario con compuesto -> desplazamiento químico.
    """
    element = self.get_element(element_symbol)
    if not element:
        return {}

    return {
        comp_name: comp.chemical_shift
        for comp_name, comp in element.compounds.items()
        if comp.chemical_shift is not None
    }
list_elements
list_elements() -> list[str]

Lista todos los elementos disponibles ordenados por número atómico.

Retorna

list[str] Lista de símbolos de elementos ordenados por Z.

Source code in src/xps_analyzer/models/reference.py
def list_elements(self) -> list[str]:
    """
    Lista todos los elementos disponibles ordenados por número atómico.

    Retorna
    -------
    list[str]
        Lista de símbolos de elementos ordenados por Z.
    """
    return sorted(
        self.elements.keys(), key=lambda symbol: self.elements[symbol].atomic_number
    )
get_statistics
get_statistics() -> dict[str, int]

Obtiene estadísticas de la base de datos.

Retorna

dict[str, int] Diccionario con estadísticas de contenido.

Source code in src/xps_analyzer/models/reference.py
def get_statistics(self) -> dict[str, int]:
    """
    Obtiene estadísticas de la base de datos.

    Retorna
    -------
    dict[str, int]
        Diccionario con estadísticas de contenido.
    """
    total_lines = sum(len(el.photoelectron_lines) for el in self.elements.values())
    total_compounds = sum(len(el.compounds) for el in self.elements.values())

    return {
        "total_elements": len(self.elements),
        "total_photoelectron_lines": total_lines,
        "total_compounds": total_compounds,
        "elements_with_compounds": sum(
            1 for el in self.elements.values() if el.compounds
        ),
    }
validate_integrity
validate_integrity() -> dict[str, list[str]]

Valida la integridad completa de la base de datos.

Retorna

dict[str, list[str]] Diccionario con warnings/errores encontrados por categoría.

Source code in src/xps_analyzer/models/reference.py
def validate_integrity(self) -> dict[str, list[str]]:
    """
    Valida la integridad completa de la base de datos.

    Retorna
    -------
    dict[str, list[str]]
        Diccionario con warnings/errores encontrados por categoría.
    """
    warnings = {
        "missing_most_useful": [],
        "no_compounds": [],
        "inconsistent_energies": [],
    }

    for symbol, element in self.elements.items():
        # Elementos sin energía más útil
        if element.binding_energy_most_useful is None:
            warnings["missing_most_useful"].append(symbol)

        # Elementos sin compuestos
        if not element.compounds:
            warnings["no_compounds"].append(symbol)

        # Energías inconsistentes entre líneas y compuestos
        for comp_name, compound in element.compounds.items():
            if compound.peak_position is not None:
                # Buscar línea del mismo orbital
                matching_line = element.get_line_by_orbital(compound.orbital)
                if matching_line is not None:
                    diff = abs(
                        matching_line.binding_energy - compound.peak_position
                    )
                    if diff > 10.0:  # Diferencia muy grande es sospechosa
                        warnings["inconsistent_energies"].append(
                            f"{symbol} {compound.orbital}: línea={matching_line.binding_energy:.1f}, "
                            f"compuesto {comp_name}={compound.peak_position:.1f} (diff={diff:.1f})"
                        )

    return warnings