Source code for esbmtk.esbmtk_base
"""esbmtk: A general purpose Earth Science box model toolkit.
Copyright (C), 2020 Ulrich G. Wortmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import time
import typing as tp
import numpy as np
import numpy.typing as npt
if tp.TYPE_CHECKING:
from .esbmtk import SpeciesProperties
# declare numpy types
NDArrayFloat = npt.NDArray[np.float64]
[docs]
class KeywordError(Exception):
"""Exception raised for errors in keyword arguments.
Parameters
----------
message : str
Explanation of the error
Examples
--------
>>> raise KeywordError("Invalid keyword 'xyz'")
"""
def __init__(self, message):
message = f"\n\n{message}\n"
super().__init__(message)
[docs]
class MissingKeywordError(Exception):
"""Exception raised when a required keyword argument is missing.
Parameters
----------
message : str
Explanation of the error
Examples
--------
>>> raise MissingKeywordError("'name' is a mandatory keyword")
"""
def __init__(self, message):
message = f"\n\n{message}\n"
super().__init__(message)
[docs]
class InputError(Exception):
"""Exception raised for errors in the input parameters.
Parameters
----------
message : str
Explanation of the error
Examples
--------
>>> raise InputError("Value must be positive")
"""
def __init__(self, message):
message = f"\n\n{message}\n"
super().__init__(message)
[docs]
class FluxSpecificationError(Exception):
"""Exception raised for errors in flux specifications.
Parameters
----------
message : str
Explanation of the error
Examples
--------
>>> raise FluxSpecificationError("Unknown flux units")
"""
def __init__(self, message):
message = f"\n\n{message}\n"
super().__init__(message)
[docs]
class SpeciesPropertiesMolweightError(Exception):
"""Exception raised when molecular weight is missing or invalid.
Parameters
----------
message : str
Explanation of the error
Examples
--------
>>> raise SpeciesPropertiesMolweightError("Missing molecular weight for C")
"""
def __init__(self, message):
message = f"\n\n{message}\n"
super().__init__(message)
[docs]
class InputParsing:
"""Provides various routines to parse and process keyword arguments.
All derived classes need to declare the allowed keyword arguments,
their default values and the type in the following format:
defaults = {"key": [value, (allowed instances)]}
The recommended sequence is to first set default values via
__register_variable_names__() and then update with provided values
using __update_dict_entries__(defaults, kwargs).
Notes
-----
This class is not meant to be instantiated directly.
"""
def __init__(self):
raise NotImplementedError("InputParsing has no instance!")
def __initialize_keyword_variables__(self, kwargs) -> None:
"""
Check, register and update keyword variables.
Parameters
----------
kwargs : dict
Dictionary of keyword arguments to process
Returns
-------
None
Examples
--------
>>> self.__initialize_keyword_variables__({"name": "test", "value": 10})
"""
self.update = False
self.__check_mandatory_keywords__(self.lrk, kwargs)
_x = kwargs.get("id", None)
self.__register_variable_names__(self.defaults, kwargs)
self.__update_dict_entries__(self.defaults, kwargs)
self.update = True
def __check_mandatory_keywords__(self, lrk: list, kwargs: dict) -> None:
"""Verify that all required keywords are present in kwargs.
Parameters
----------
lrk : list
List of required keywords or lists of alternative keywords
kwargs : dict
Dictionary of provided keyword arguments
Raises
------
MissingKeywordError
If a required keyword is missing
ValueError
If exactly one of a list of alternative keywords is not provided
TypeError
If lrk is not a list or kwargs is not a dictionary
Notes
-----
If an element of lrk is a list, it represents alternative keywords
where exactly one must be provided.
Examples
--------
>>> self.__check_mandatory_keywords__(["name", ["option1", "option2"]],
{"name": "test", "option1": "value"})
"""
if not lrk:
return
# Type checking using the newer isinstance syntax
if not isinstance(lrk, list):
raise TypeError(f"Required keywords list must be a list, not {type(lrk)}")
if not isinstance(kwargs, dict):
raise TypeError(f"Keywords must be a dictionary, not {type(kwargs)}")
for key in lrk:
if isinstance(key, list):
# This is a list of alternative keywords - exactly one must be provided
valid_keys = [k for k in key if k in kwargs and kwargs[k] != "None"]
if len(valid_keys) == 0:
# Use context preservation for exceptions
try:
alternatives = ", ".join(key)
raise ValueError(
f"No valid alternatives found among: {alternatives}"
)
except ValueError as err:
raise MissingKeywordError(
f"At least one of these keywords must be provided: {key}"
) from err
elif len(valid_keys) > 1:
try:
choices = ", ".join(valid_keys)
raise ValueError(f"Multiple choices provided: {choices}")
except ValueError as err:
raise ValueError(
f"Only one of these keywords should be provided: {key}. "
f"Found: {valid_keys}"
) from err
elif key not in kwargs:
raise MissingKeywordError(f"'{key}' is a mandatory keyword")
elif kwargs[key] is None:
raise MissingKeywordError(
f"'{key}' is a mandatory keyword and cannot be None"
)
def __register_variable_names__(
self,
defaults: dict[str, list[any, tuple]],
kwargs: dict,
) -> None:
"""Register the key-value pairs as local instance variables.
We register them with their actual variable name and as _variable_name
to support setter and getter methods and avoid name conflicts.
Parameters
----------
defaults : dict
Dictionary with default values and allowed types
kwargs : dict
Dictionary of keyword arguments
Returns
-------
None
Examples
--------
>>> defaults = {"name": ["default", (str,)], "value": [0, (int, float)]}
>>> self.__register_variable_names__(defaults, {})
"""
for key, value in defaults.items():
if len(value) > 2: # check for properties
if value[2]:
setattr(self, f"_{key}", value[0])
else:
setattr(self, f"{key}", value[0])
else:
setattr(self, key, value[0])
# save kwargs dict
self.kwargs: dict = kwargs
def __update_dict_entries__(
self,
defaults: dict[str, list[any, tuple]],
kwargs: dict[str, any],
) -> None:
"""
Validate and update instance attributes with provided keyword arguments.
Parameters
----------
defaults : dict
Dictionary with format {"key": [default_value, (allowed_types)]}
kwargs : dict
Dictionary with format {"key": value}
Raises
------
KeywordError
If a key in kwargs is not in defaults
InputError
If a value in kwargs is not of the expected type or fails validation
ValueError
If defaults dictionary is empty
Notes
-----
This function assumes that all defaults have been registered with the
instance via __register_variable_names__()
Examples
--------
>>> defaults = {"name": ["default", (str,)], "value": [0, (int, float)]}
>>> self.__update_dict_entries__(defaults, {"name": "test", "value": 10})
"""
self.__validate_defaults_and_kwargs__(defaults, kwargs)
if not kwargs:
return # Nothing to update
for key, value in kwargs.items():
self.__process_keyword__(defaults, key, value)
def __validate_defaults_and_kwargs__(self, defaults, kwargs):
"""
Validate that defaults dictionary is not empty.
Parameters
----------
defaults : dict
Dictionary with default values
kwargs : dict
Dictionary of keyword arguments
Raises
------
ValueError
If defaults dictionary is empty
"""
if not defaults:
raise ValueError("Defaults dictionary cannot be empty")
def __process_keyword__(self, defaults, key, value):
"""
Process a single keyword argument.
Parameters
----------
defaults : dict
Dictionary with format {"key": [default_value, (allowed_types)]}
key : str
The keyword to process
value : any
The value to validate and set
Raises
------
KeywordError
If key is not in defaults
InputError
If value is not of the expected type or fails validation
"""
# Check if the key exists in defaults
if key not in defaults:
raise KeywordError(f"'{key}' is not a valid keyword")
# Skip None values as they're handled elsewhere
if value is None: # or value == "None":
return
# Get the expected types
expected_types = defaults[key][1]
# Validate the value type
self.__validate_value_type__(key, value, expected_types)
# Perform additional validation based on type
try:
self._validate_value(key, value, expected_types)
except Exception as err:
raise InputError(f"Validation failed for '{key}': {str(err)}") from err
# Update the values
self.__update_attribute_values__(defaults, key, value)
def __validate_value_type__(self, key, value, expected_types):
"""
Validate that a value is of the expected type.
Parameters
----------
key : str
The keyword being validated
value : any
The value to validate
expected_types : type or tuple of types
The expected types for the value
Raises
------
InputError
If value is not of the expected type
"""
if not isinstance(value, expected_types):
try:
actual_type = type(value).__name__
if isinstance(expected_types, tuple):
expected_types_str = ", ".join(t.__name__ for t in expected_types)
else:
expected_types_str = expected_types.__name__
raise TypeError(
f"Value '{value}' has type {actual_type}, expected {expected_types_str}"
)
except TypeError as err:
raise InputError(
f"'{value}' for '{key}' must be of type {expected_types_str}, "
f"not {actual_type}"
) from err
def __update_attribute_values__(self, defaults, key, new_value):
"""
Update the attribute values in both defaults dictionary and instance.
Parameters
----------
defaults : dict
Dictionary with format {"key": [default_value, (allowed_types)]}
key : str
The keyword to update
value : any
The value to set
"""
current_value = defaults[key]
defaults[key][0] = new_value # update defaults dictionary
if len(current_value) > 2:
if current_value[2]:
setattr(self, f"_{key}", new_value)
else:
setattr(self, key, new_value) # update instance variables
else:
setattr(self, key, new_value)
def _validate_value(self, key, value, expected_types):
"""
Perform additional validation on values based on their types.
Parameters
----------
key : str
The keyword being validated
value : any
The value to validate
expected_types : type or tuple of types
The expected types for the value
Raises
------
ValueError
If the value fails validation
Examples
--------
>>> self._validate_value("volume", 10.5, (int, float))
"""
# String validations
if isinstance(value, str):
if key == "name" and (not value or value.isspace()):
raise ValueError("Name cannot be empty or just whitespace")
# Numeric validations
elif isinstance(value, int | float): # Using newer isinstance syntax
# Example: values that should be positive
if key in ("volume", "m_weight", "salinity", "temperature") and value <= 0:
raise ValueError(f"'{key}' must be positive, got {value}")
# Example: values that should be within a specific range
if key == "ph" and (value < 0 or value > 14):
raise ValueError(f"pH must be between 0 and 14, got {value}")
def __register_with_parent__(self) -> None:
"""
Register self as attribute of self.parent and set full name.
If self.parent == "None", full_name = name.
Otherwise, full_name = parent.full_name + "." + self.name
Returns
-------
None
Examples
--------
>>> self.__register_with_parent__()
"""
if self.parent == "None":
self.full_name = self.name
reg = self
else:
self.full_name = f"{self.parent.full_name}.{self.name}"
reg = self.parent.model
if self.full_name in reg.lmo:
print("\n -------------- Warning ------------- \n")
print(f"\t {self.full_name} is a duplicate name in reg.lmo")
print("\n ---------------------- ------------- \n")
# register with model
reg.lmo.append(self.full_name)
reg.lmo2.append(self)
reg.dmo.update({self.full_name: self})
setattr(self.parent, self.name, self)
self.kwargs["full_name"] = self.full_name
self.reg_time = time.monotonic()
def __test_and_resolve_duplicates__(self, name, lmo):
"""
Test for duplicate instance names and resolve by appending _1, _2, etc.
Parameters
----------
name : str
The instance name to check
lmo : list
List of existing instance names
Returns
-------
tuple
(resolved_name, updated_lmo_list)
Examples
--------
>>> name, lmo = self.__test_and_resolve_duplicates__("test", ["existing"])
>>> print(name)
test
"""
if name in lmo:
print(f"\n Warning, {name} is a duplicate, trying with {name}_1\n")
name = name + "_1"
name, lmo = self.__test_and_resolve_duplicates__(name, lmo)
else:
lmo.append(name)
return name, lmo
[docs]
class esbmtkBase(InputParsing):
"""The esbmtk base class template.
This class handles keyword arguments, name registration and
other common tasks.
Examples
--------
.. code-block:: python
# Define required keywords in lrk list
self.lrk: tp.List = ["name"]
# Define allowed type per keyword in defaults dict
self.defaults: dict[str, list[any, tuple]] = {
"name": ["None", (str)],
"model": ["None", (str, Model)],
"salinity": [35, (int, float)], # int or float
}
# Parse and register all keywords with the instance
self.__initialize_keyword_variables__(kwargs)
# Register the instance
self.__register_with_parent__()
"""
def __init__(self) -> None:
raise NotImplementedError
def __repr__(self, log=0) -> str:
"""Return string representation of the object.
Parameters
----------
log : int, default=0
If 0 and object was just created (<1 second ago),
returns empty string to suppress output
Returns
-------
str
String representation of the object
Examples
--------
>>> repr(obj)
'ClassName(name = "example", value = 42)'
"""
from esbmtk import Q_
m: str = ""
# suppress output during object initialization
tdiff = time.monotonic() - self.reg_time
m = f"{self.__class__.__name__}(\n"
for k, v in self.kwargs.items():
if not isinstance({k}, esbmtkBase):
# check if this is not another esbmtk object
if "esbmtk" in str(type(v)):
m = f"{m} {k} = {v.name},\n"
elif isinstance(v, str | Q_): # Using newer isinstance syntax
m = f"{m} {k} = '{v}',\n"
elif isinstance(v, list | np.ndarray): # Using newer isinstance syntax
m = f"{m} {k} = '{v[:3]}',\n"
else:
m = f"{m} {k} = {v},\n"
m = "" if log == 0 and tdiff < 1 else f"{m})"
return m
def __str__(self, kwargs=None):
"""Return a string representation of the object with its key attributes.
Parameters
----------
kwargs : dict, optional
Additional keyword arguments
- indent : int
Number of spaces to indent output
- index : int
Index to display for array values, default=-2
Returns
-------
str
Formatted string representation of the object
Examples
--------
>>> str(obj)
'example (ClassName)
value = 42'
"""
if kwargs is None:
kwargs = {}
from esbmtk import Q_
m: str = ""
off: str = " "
ind: str = kwargs["indent"] * " " if "indent" in kwargs else ""
index = int(kwargs["index"]) if "index" in kwargs else -2
m = f"{ind}{self.name} ({self.__class__.__name__})\n"
for k, v in self.kwargs.items():
if not isinstance({k}, esbmtkBase):
# check if this is not another esbmtk object
if "esbmtk" in str(type(v)):
pass
elif isinstance(v, str) and k != "name" or isinstance(v, Q_):
m = f"{m}{ind}{off}{k} = {v}\n"
elif isinstance(v, np.ndarray):
m = f"{m}{ind}{off}{k}[{index}] = {v[index]:.2e}\n"
elif k != "name":
m = f"{m}{ind}{off}{k} = {v}\n"
return m
def __lt__(self, other) -> bool: # Fixed return type annotation from None to bool
"""Compare if self is less than other for sorting with sorted().
Parameters
----------
other : esbmtkBase
Object to compare with
Returns
-------
bool
True if self.n < other.n, False otherwise
Examples
--------
>>> sorted([obj2, obj1]) # If obj1.n < obj2.n, returns [obj1, obj2]
[obj1, obj2]
"""
return self.n < other.n
def __gt__(self, other) -> bool: # Fixed return type annotation from None to bool
"""Compare if self is greater than other for sorting with sorted().
Parameters
----------
other : esbmtkBase
Object to compare with
Returns
-------
bool
True if self.n > other.n, False otherwise
Examples
--------
>>> sorted([obj1, obj2], reverse=True) # If obj2.n > obj1.n, returns [obj2, obj1]
[obj2, obj1]
"""
return self.n > other.n
def __add_flux__(self, lof, flux):
"""Check if flux is already defined.
Otherwise add flux to lof
"""
if flux in lof:
raise FluxSpecificationError(f"Duplicate entry for {flux.name}")
else:
if flux is not None:
lof.append(flux)
[docs]
def info(self, **kwargs) -> None:
"""Show an overview of the object properties.
Parameters
----------
**kwargs : dict, optional
Additional keyword arguments
----------------------------
* ``indent`` : int
Number of spaces for indentation
Returns
-------
None
Examples
--------
.. code-block:: python
obj.info(indent=2)
"""
if "indent" not in kwargs:
indent = 0
ind = ""
else:
indent = kwargs["indent"]
ind = " " * indent
# print basic data about this object
print(f"{ind}{self.__str__(kwargs)}")
def __aux_inits__(self) -> None:
"""Auxiliary initialization code.
This method is a placeholder for additional initialization steps
that subclasses might implement.
Returns
-------
None
Examples
--------
>>> self.__aux_inits__()
"""
pass
[docs]
def ensure_q(self, arg):
"""Ensure that a given input argument is a quantity object.
Parameters
----------
arg : str, Quantity, or numeric
The argument to convert to a Quantity
Returns
-------
Quantity
The input argument as a Quantity object
Raises
------
InputError
If the argument is None, empty, or cannot be converted to a Quantity
Examples
--------
>>> self.ensure_q("10 kg")
<Quantity(10, 'kilogram')>
>>> self.ensure_q(existing_quantity)
<Quantity(existing_quantity)>
"""
from esbmtk import Q_
# Check if argument is None or empty
if arg is None:
raise InputError("Cannot convert None to a Quantity")
if isinstance(arg, str) and not arg.strip():
raise InputError("Cannot convert empty string to a Quantity")
# Convert based on type
if isinstance(arg, Q_):
return arg
elif isinstance(arg, str):
try:
return Q_(arg)
except Exception as err:
raise InputError(
f"Failed to convert '{arg}' to a Quantity: {str(err)}"
) from err
elif isinstance(arg, int | float): # Using newer isinstance syntax
# If only a number is provided with no units, raise an error
raise InputError(
f"Numeric value {arg} provided without units. "
f"Please provide a string with units, e.g., '{arg} kg'"
)
else:
raise InputError(
f"Cannot convert type {type(arg)} to a Quantity. "
f"Must be a string or Quantity."
)
[docs]
def help(self) -> None:
"""
Show all keywords, their default values and allowed types.
Prints information about all available keywords and
highlights which ones are mandatory.
Returns
-------
None
Examples
--------
>>> obj.help()
"""
print(f"\n{self.full_name} has the following keywords:\n")
for k, v in self.defaults_copy.items():
print(f"{k} defaults to {v[0]}, allowed types = {v[1]}")
print()
print("The following keywords are mandatory:")
for kw in self.lrk:
print(f"{kw}")
[docs]
def set_flux(self, mass: str, time: str, substance: SpeciesProperties):
"""
Convert a flux rate to model units (kg/s or mol/s).
Parameters
----------
mass : str
Mass value with units, e.g., "12 Tmol" or "500 kg"
time : str
Time unit, e.g., "year", "day", "s"
substance : SpeciesProperties
Species properties object containing molecular weight information
Returns
-------
Quantity
Flux rate in model units (mol/time or g/time)
Raises
------
InputError
If input parameters are None, empty, or of incorrect type
FluxSpecificationError
If unit conversion cannot be performed
SpeciesPropertiesMolweightError
If substance has no molecular weight defined
Examples
--------
>>> M.set_flux("12 Tmol", "year", M.C)
<Quantity(12, 'teramol / year')>
Notes
-----
If model mass units are in mol, no mass unit conversion will be made.
If model mass units are in kg, the flux will be converted accordingly.
"""
# Validate all input parameters first
self.__validate_flux_inputs__(mass, time, substance)
# Validate substance properties
self.__validate_substance_properties__(substance)
# Convert the mass quantity
r = self.__convert_mass_to_model_units__(mass, substance)
# Apply the time unit and return the result
return self.__apply_time_unit__(r, time)
def __validate_flux_inputs__(self, mass, time, substance):
"""
Validate the input parameters for set_flux.
Parameters
----------
mass : str or Quantity
Mass value with units
time : str
Time unit
substance : SpeciesProperties
Species properties object
Raises
------
InputError
If any input parameter is invalid
"""
from esbmtk import Q_, ureg
# Check for empty inputs
if not mass:
raise InputError("Mass parameter cannot be empty")
if not time:
raise InputError("Time parameter cannot be empty")
if substance is None:
raise InputError("Substance parameter cannot be None")
# Type checks
if not isinstance(mass, str | Q_):
raise InputError(f"Mass must be a string or Quantity, not {type(mass)}")
if not isinstance(time, str):
raise InputError(f"Time must be a string, not {type(time)}")
# Validate time units
try:
ureg(time)
except Exception as err:
raise InputError(f"Invalid time unit: '{time}'. Error: {str(err)}") from err
def __validate_substance_properties__(self, substance):
"""
Validate that substance has the required properties.
Parameters
----------
substance : SpeciesProperties
Species properties object
Raises
------
SpeciesPropertiesMolweightError
If substance properties are missing or invalid
"""
# Check for m_weight attribute
if not hasattr(substance, "m_weight"):
raise SpeciesPropertiesMolweightError(
f"Substance {getattr(substance, 'full_name', str(substance))} has no 'm_weight' attribute"
)
# Check for model unit attributes
if not hasattr(substance, "mo") or not hasattr(substance.mo, "m_unit"):
raise SpeciesPropertiesMolweightError(
f"Substance {getattr(substance, 'full_name', str(substance))} has incomplete model unit definition"
)
# Check m_weight value
if substance.m_weight <= 0:
raise SpeciesPropertiesMolweightError(
f"No molecular weight definition for {substance.full_name} (m_weight={substance.m_weight})"
)
def __convert_mass_to_model_units__(self, mass, substance):
"""
Convert mass to model units using substance properties.
Parameters
----------
mass : str or Quantity
Mass value with units
substance : SpeciesProperties
Species properties object
Returns
-------
Quantity
Mass converted to model units
Raises
------
FluxSpecificationError
If unit conversion fails
"""
from esbmtk import Q_, ureg
try:
mass = Q_(mass) if isinstance(mass, str) else mass
g_per_mol = ureg("g/mol")
if mass.is_compatible_with("g") or mass.is_compatible_with("mol"):
return mass.to(
substance.mo.m_unit, # target unit (mol)
"chemistry", # context
mw=substance.m_weight * g_per_mol, # g/mol
)
else:
raise FluxSpecificationError(
f"No known conversion for {mass} (units: {mass.units}) and {substance.full_name}"
)
except Exception as err:
if isinstance(err, FluxSpecificationError):
raise
else:
raise FluxSpecificationError(
f"Failed to convert {mass} for {substance.full_name}: {str(err)}"
) from err
def __apply_time_unit__(self, quantity, time_unit):
"""
Apply time unit to a quantity to get a rate.
Parameters
----------
quantity : Quantity
The quantity to convert to a rate
time_unit : str
The time unit to apply
Returns
-------
Quantity
Rate with the specified time unit
Raises
------
FluxSpecificationError
If applying the time unit fails
"""
from esbmtk import ureg
try:
return quantity / ureg(time_unit)
except Exception as err:
raise FluxSpecificationError(
f"Failed to apply time unit '{time_unit}': {str(err)}"
) from err
def __update_ode_constants__(self, value: int | float, vname: str) -> int:
"""Replace a value to the global parameter list."""
key = f"{self.full_name}_{vname}"
index = self.model.doc.get(key, None)
if index != None:
self.model.toc = (
self.model.toc[:index] + (value,) + self.model.toc[index + 1 :]
)
def __add_to_ode_constants__(self, value: int | float, vname: str) -> int:
"""
Add a value to the global parameter list and track its index.
Parameters
----------
value : any
Value to add to the parameter list
Returns
-------
int
Index position of the value in the parameter list
Raises
------
AttributeError
If the model attribute is missing or does not have required properties
TypeError
If model.toc is not a sequence that can be extended
Examples
--------
>>> index = self.__add_to_ode_constants__(42.0)
>>> print(index)
5 # If this was the 6th constant added (zero-indexed)
"""
# Check if model attribute exists
if not hasattr(self, "model"):
raise AttributeError(
"Cannot update ODE constants: 'model' attribute is missing"
)
# Check if model has the required attributes
if not hasattr(self.model, "toc"):
raise AttributeError(
"Model object is missing 'toc' attribute required for ODE constants"
)
if not hasattr(self.model, "gcc"):
raise AttributeError(
"Model object is missing 'gcc' attribute required for ODE constants"
)
# Add the value to the parameter list if it's not "None"
if value != "None":
# Validate that toc is a sequence that can be extended
try:
if not hasattr(self.model.toc, "__iter__"):
raise TypeError("Model.toc must be an iterable")
except TypeError as err:
raise TypeError(
f"Model.toc must be a sequence, not {type(self.model.toc)}"
) from err
# Update the model's toc tuple with the new value
self.model.toc = (*self.model.toc, value)
# Get the current index
index = self.model.gcc
# update index dict
name = f"{self.full_name}_{vname}"
self.model.doc[name] = index
# Increment the counter
self.model.gcc = self.model.gcc + 1
else:
index = 0
return index
[docs]
def validate(self) -> bool:
"""
Validate the object state after initialization.
This method performs comprehensive validation of the object's attributes
to ensure they are in a consistent and valid state.
Returns
-------
bool
True if the object is valid
Raises
------
ValueError
If the object fails validation
AttributeError
If required attributes are missing
Examples
--------
>>> obj = SomeClass(name="test", value=10)
>>> obj.validate() # Returns True if valid, raises exception if not
True
"""
# Check for required attributes
required_attrs = ["name", "full_name"]
for attr in required_attrs:
try:
if not hasattr(self, attr):
raise AttributeError(f"Required attribute '{attr}' is missing")
value = getattr(self, attr)
if value in (None, "None", ""):
raise ValueError(f"Required attribute '{attr}' cannot be empty")
except (AttributeError, ValueError) as err:
raise ValueError(
f"Required attribute '{attr}' is missing or empty"
) from err
# Validate specific attributes based on their expected properties
if hasattr(self, "volume") and hasattr(self, "model"):
from esbmtk import Q_
# Example: validate that volume has compatible units with the model
try:
if (
hasattr(self, "volume")
and hasattr(self, "model")
and isinstance(self.volume, Q_)
and hasattr(self.model, "v_unit")
and not self.volume.is_compatible_with(self.model.v_unit)
):
raise ValueError(
f"Volume units '{self.volume.units}' are incompatible with model units '{self.model.v_unit}'"
)
except Exception as err:
raise ValueError(
f"Volume units {self.volume.units} are not compatible "
f"with model volume units {self.model.v_unit}"
) from err
# Add other validation rules specific to your application
# If all validations pass, return True
return True