diff --git a/drux/__init__.py b/drux/__init__.py index e6788d6..69fb951 100644 --- a/drux/__init__.py +++ b/drux/__init__.py @@ -2,10 +2,12 @@ """Drux modules.""" from .params import DRUX_VERSION -from .higuchi import HiguchiModel, HiguchiParameters -from .zero_order import ZeroOrderModel, ZeroOrderParameters -from .first_order import FirstOrderModel, FirstOrderParameters -from .weibull import WeibullModel, WeibullParameters -from .hopfenberg import HopfenbergModel, HopfenbergParameters __version__ = DRUX_VERSION + +from .higuchi import HiguchiModel +from .zero_order import ZeroOrderModel +from .first_order import FirstOrderModel +from .weibull import WeibullModel +from .hopfenberg import HopfenbergModel + diff --git a/drux/base_model.py b/drux/base_model.py index 2eee8fa..3732cc3 100644 --- a/drux/base_model.py +++ b/drux/base_model.py @@ -2,7 +2,7 @@ import numpy as np import matplotlib.pyplot as plt -from abc import ABC, abstractmethod +from types import SimpleNamespace from typing import Any, Optional from .messages import ( @@ -14,17 +14,19 @@ ERROR_TARGET_RELEASE_EXCEEDS_MAX, ) +from .registry import get_model_config -class DrugReleaseModel(ABC): - """ - Abstract base class for drug release models. + +class DrugReleaseModel: + """Base class for drug release models. This class provides a common interface and functionality for various mathematical models of drug release from delivery systems. - Subclasses should implement: - - _model_function(): Core model equation - - _validate_parameters(): Parameter validation + Model classes are typically generated via :func:`create_model_class` + from the centralized model registry. All equation logic, + parameter validation, and parameter metadata live in the registry + so that subclasses need no custom overrides. """ def __init__(self): @@ -35,25 +37,40 @@ def __init__(self): "xlabel": "Time (s)", "ylabel": "Cumulative Release", "title": "Drug Release Profile", - "label": "Release Profile"} + "label": "Release Profile", + } + self._parameters = None + self._model_name = None - @abstractmethod def _validate_parameters(self) -> None: - """ - Validate model parameters. - - Should raise ValueError if parameters are invalid. - """ - pass + """Validate model parameters using the model registry.""" + if self._model_name is None or self._parameters is None: + raise ValueError("Model name and parameters must be set") + + config = get_model_config(self._model_name) + + for param_name, rule in config["validation"].items(): + if rule.get("cross_param"): + # Cross-parameter validation (e.g., cs <= c0) + if not rule["check"](self._parameters): + raise ValueError(rule["error"]) + else: + # Single parameter validation + param_value = getattr(self._parameters, param_name) + if not rule["check"](param_value): + raise ValueError(rule["error"]) - @abstractmethod def _model_function(self, t: float) -> float: """ Model function that calculates drug release profile over time. :param t: time point at which to calculate drug release """ - pass + if self._model_name is None or self._parameters is None: + raise ValueError("Model name and parameters must be set") + + config = get_model_config(self._model_name) + return config["equation"](self._parameters, t) def _get_release_profile(self) -> np.ndarray: """Calculate the drug release profile over the specified time points.""" @@ -91,13 +108,14 @@ def simulate(self, duration: int, time_step: float = 1) -> np.ndarray: return self._release_profile def plot( - self, - show: bool = True, - label: Optional[str] = None, - xlabel: Optional[str] = None, - ylabel: Optional[str] = None, - title: Optional[str] = None, - **kwargs: Any) -> tuple: + self, + show: bool = True, + label: Optional[str] = None, + xlabel: Optional[str] = None, + ylabel: Optional[str] = None, + title: Optional[str] = None, + **kwargs: Any + ) -> tuple: """ Plot the drug release profile. @@ -112,7 +130,10 @@ def plot( # Plotting the release profile ax.plot( - self._time_points, self._release_profile, label=label or self._plot_parameters["label"], **kwargs + self._time_points, + self._release_profile, + label=label or self._plot_parameters["label"], + **kwargs ) ax.set_xlabel(xlabel or self._plot_parameters["xlabel"]) ax.set_ylabel(ylabel or self._plot_parameters["ylabel"]) @@ -160,3 +181,60 @@ def time_for_release(self, target_release: float) -> float: # Find first time point where release >= target idx = np.argmax(self._release_profile >= target_release) return self._time_points[idx] + + +def _create_parameters(**kwargs): + """Create a parameters namespace. + + :param kwargs: Parameter values + """ + return SimpleNamespace(**kwargs) + + +def create_model_class(model_name: str, class_name: str, label: str, docstring: str = "") -> type: + """Generate a :class:`DrugReleaseModel` subclass from the model registry. + + :param model_name: Key in the model registry (e.g. ``"zero_order"``) + :param class_name: Name of the generated class (e.g. ``"ZeroOrderModel"``) + :param label: Plot legend label (e.g. ``"Zero-Order Model"``) + :param docstring: Docstring for the generated class + """ + config = get_model_config(model_name) + param_specs = config["params"] + + # Build the ordered list of (name, type, default|REQUIRED) for __init__ + required_params = [] + optional_params = [] + for pname, pinfo in param_specs.items(): + if pinfo["default"] is not None: + optional_params.append((pname, pinfo["type"], pinfo["default"])) + else: + required_params.append((pname, pinfo["type"])) + + def __init__(self, **kwargs): + # Apply defaults for missing optional params + for pname, _ptype, pdefault in optional_params: + kwargs.setdefault(pname, pdefault) + # Check all required params are present + for pname, _ptype in required_params: + if pname not in kwargs: + raise TypeError(f"Missing required parameter: {pname}") + DrugReleaseModel.__init__(self) + self._model_name = model_name + self._parameters = _create_parameters(**kwargs) + self._plot_parameters["label"] = label + + def __repr__(self): + parts = ", ".join( + f"{pname}={getattr(self._parameters, pname)}" + for pname in param_specs + ) + return f"drux.{class_name}({parts})" + + cls = type(class_name, (DrugReleaseModel,), { + "__init__": __init__, + "__repr__": __repr__, + "__doc__": docstring or f"Simulator for the {label.lower()}.", + }) + + return cls diff --git a/drux/first_order.py b/drux/first_order.py index 40e6215..29eb081 100644 --- a/drux/first_order.py +++ b/drux/first_order.py @@ -1,61 +1,11 @@ # -*- coding: utf-8 -*- """Drux first-order model implementation.""" -from math import exp -from .base_model import DrugReleaseModel -from .messages import ERROR_FIRST_ORDER_INITIAL_AMOUNT, ERROR_FIRST_ORDER_RELEASE_RATE -from dataclasses import dataclass +from .base_model import create_model_class -@dataclass -class FirstOrderParameters: - """ - Parameters for the first-order model. - - Attributes: - M0 (float): entire releasable amount of drug (normally M0 > 0) (mg) - k (float): first-order release rate constant (1/s) - """ - - M0: float - k: float - - -class FirstOrderModel(DrugReleaseModel): - """Simulator for the first-order drug release model.""" - - def __init__(self, k: float, M0: float) -> None: - """ - Initialize the first-order model with the given parameters. - - :param k: first-order release rate constant (1/s) - :param M0: entire releasable amount of drug (the asymptotic maximum) (mg) - """ - super().__init__() - self._parameters = FirstOrderParameters(k=k, M0=M0) - self._plot_parameters["label"] = "First-Order Model" - - def __repr__(self): - """Return a string representation of the First-Order model.""" - return f"drux.FirstOrderModel(k={self._parameters.k}, M0={self._parameters.M0})" - - def _model_function(self, t: float) -> float: - """ - Calculate the drug release at time t using the first-order model. - - Formula: - - M(t) = M0 * (1 - exp(-k * t)) - :param t: time (s) - """ - M0 = self._parameters.M0 - k = self._parameters.k - - Mt = M0 * (1 - exp(-k * t)) - - return Mt - - def _validate_parameters(self) -> None: - """Validate the parameters of the first-order model.""" - if self._parameters.M0 < 0: - raise ValueError(ERROR_FIRST_ORDER_INITIAL_AMOUNT) - if self._parameters.k < 0: - raise ValueError(ERROR_FIRST_ORDER_RELEASE_RATE) +FirstOrderModel = create_model_class( + model_name="first_order", + class_name="FirstOrderModel", + label="First-Order Model", + docstring="Simulator for the first-order drug release model.", +) diff --git a/drux/higuchi.py b/drux/higuchi.py index 9df2fbf..e8f7966 100644 --- a/drux/higuchi.py +++ b/drux/higuchi.py @@ -1,78 +1,11 @@ # -*- coding: utf-8 -*- """Drux Higuchi model implementation.""" -from .base_model import DrugReleaseModel -from .messages import ( - ERROR_INVALID_DIFFUSION, - ERROR_INVALID_CONCENTRATION, - ERROR_INVALID_SOLUBILITY, - ERROR_SOLUBILITY_HIGHER_THAN_CONCENTRATION, -) -from dataclasses import dataclass -from math import sqrt - - -@dataclass -class HiguchiParameters: - """ - Parameters for the Higuchi model based on physical formulation. - - Attributes: - D (float): Drug diffusivity in the polymer carrier (cm^2/s) - c0 (float): Initial drug concentration (mg/cm^3) - cs (float): Drug solubility in the polymer (mg/cm^3) - """ - - D: float - c0: float - cs: float - - -class HiguchiModel(DrugReleaseModel): - """Simulator for the Higuchi drug release model using analytical expressions based on concentration conditions.""" - - def __init__(self, D: float, c0: float, cs: float) -> None: - """ - Initialize the Higuchi model with the given parameters. +from .base_model import create_model_class - :param D: Drug diffusivity in the polymer carrier (cm^2/s) - :param c0: Initial drug concentration (mg/cm^3) - :param cs: Drug solubility in the polymer (mg/cm^3) - """ - super().__init__() - self._parameters = HiguchiParameters(D=D, c0=c0, cs=cs) - self._plot_parameters["label"] = "Higuchi Model" - - def __repr__(self): - """Return a string representation of the Higuchi model.""" - return ( - f"drux.HiguchiModel(D={self._parameters.D}, " - f"c0={self._parameters.c0}, cs={self._parameters.cs})" - ) - - def _model_function(self, t: float) -> float: - """ - Calculate the drug release at time t using the Higuchi model. - - Formula: - - General case: Mt = sqrt(D * c0 * (2*c0 - cs) * cs * t) - :param t: time (s) - """ - D = self._parameters.D - c0 = self._parameters.c0 - cs = self._parameters.cs - - Mt = sqrt(D * (2 * c0 - cs) * cs * t) - - return Mt - - def _validate_parameters(self) -> None: - """Validate the parameters of the Higuchi model.""" - if self._parameters.D <= 0: - raise ValueError(ERROR_INVALID_DIFFUSION) - if self._parameters.c0 <= 0: - raise ValueError(ERROR_INVALID_CONCENTRATION) - if self._parameters.cs <= 0: - raise ValueError(ERROR_INVALID_SOLUBILITY) - if self._parameters.cs > self._parameters.c0: - raise ValueError(ERROR_SOLUBILITY_HIGHER_THAN_CONCENTRATION) +HiguchiModel = create_model_class( + model_name="higuchi", + class_name="HiguchiModel", + label="Higuchi Model", + docstring="Simulator for the Higuchi drug release model using analytical expressions based on concentration conditions.", +) diff --git a/drux/hopfenberg.py b/drux/hopfenberg.py index 89074f9..d11015b 100644 --- a/drux/hopfenberg.py +++ b/drux/hopfenberg.py @@ -1,92 +1,11 @@ # -*- coding: utf-8 -*- """Drux Hopfenberg model implementation.""" -from .base_model import DrugReleaseModel -from .messages import ( - ERROR_INVALID_EROSION_CONSTANT, - ERROR_INVALID_INITIAL_RADIUS, - ERROR_INVALID_GEOMETRY_FACTOR, - ERROR_INVALID_CONCENTRATION, - ERROR_RELEASABLE_AMOUNT, -) -from dataclasses import dataclass - - -@dataclass -class HopfenbergParameters: - """ - Parameters for the Hopfenberg model based on surface erosion. - - Attributes: - k0 (float): Erosion rate constant (mg/(mm^2·s)) - c0 (float): Initial drug concentration in the matrix (mg/mm^3) - a0 (float): Initial radius or half-thickness of the device (mm) - n (int): Geometry factor (1=slab, 2=cylinder, 3=sphere) - """ - - M: float - k0: float - c0: float - a0: float - n: int - - -class HopfenbergModel(DrugReleaseModel): - """Simulator for the Hopfenberg drug release model for surface-eroding polymers.""" - - def __init__(self, M: float, k0: float, c0: float, a0: float, n: int) -> None: - """ - Initialize the Hopfenberg model with the given parameters. - - :param M: entire releasable amount of drug (normally M > 0) (mg) - :param k0: Erosion rate constant (mg/(mm^2·s)) - :param c0: Initial drug concentration in the matrix (mg/mm^3) - :param a0: Initial radius or half-thickness of the device (mm) - :param n: Geometry factor (1=slab, 2=cylinder, 3=sphere) - """ - super().__init__() - self._parameters = HopfenbergParameters(M=M, k0=k0, c0=c0, a0=a0, n=n) - self._plot_parameters["label"] = "Hopfenberg Model" +from .base_model import create_model_class - def __repr__(self): - """Return a string representation of the Hopfenberg model.""" - return ( - f"drux.HopfenbergModel(M={self._parameters.M}, k0={self._parameters.k0}, " - f"c0={self._parameters.c0}, a0={self._parameters.a0}, " - f"n={self._parameters.n})" - ) - - def _model_function(self, t: float) -> float: - """ - Calculate the fractional drug release at time t using the Hopfenberg model. - - Formula: - - Mt = M∞(1 - (1 - k0*t / (c0*a0))^n) - - :param t: time (s) - :return: drug release - """ - M = self._parameters.M - k0 = self._parameters.k0 - c0 = self._parameters.c0 - a0 = self._parameters.a0 - n = self._parameters.n - - inner_term = 1 - (k0 * t) / (c0 * a0) - - Mt = M * (1 - (inner_term**n)) - - return Mt - - def _validate_parameters(self) -> None: - """Validate the parameters of the Hopfenberg model.""" - if self._parameters.M < 0: - raise ValueError(ERROR_RELEASABLE_AMOUNT) - if self._parameters.k0 < 0: - raise ValueError(ERROR_INVALID_EROSION_CONSTANT) - if self._parameters.c0 <= 0: - raise ValueError(ERROR_INVALID_CONCENTRATION) - if self._parameters.a0 <= 0: - raise ValueError(ERROR_INVALID_INITIAL_RADIUS) - if self._parameters.n not in (1, 2, 3): - raise ValueError(ERROR_INVALID_GEOMETRY_FACTOR) +HopfenbergModel = create_model_class( + model_name="hopfenberg", + class_name="HopfenbergModel", + label="Hopfenberg Model", + docstring="Simulator for the Hopfenberg drug release model for surface-eroding polymers.", +) diff --git a/drux/messages.py b/drux/messages.py index 9d7b189..387aa4d 100644 --- a/drux/messages.py +++ b/drux/messages.py @@ -36,11 +36,11 @@ # Error messages for Weibull ERROR_WEIBULL_SCALE_PARAMETER = "Scale parameter (a) must be positive." ERROR_WEIBULL_SHAPE_PARAMETER = "Shape parameter (b) must be positive." -ERROR_RELEASABLE_AMOUNT = ( - "Entire releasable amount of drug (M) must be non-negative." -) +ERROR_RELEASABLE_AMOUNT = "Entire releasable amount of drug (M) must be non-negative." # Hopfenberg model error messages ERROR_INVALID_EROSION_CONSTANT = "Erosion rate constant (k0) must be non-negative." ERROR_INVALID_INITIAL_RADIUS = "Initial radius or half-thickness (a0) must be positive." -ERROR_INVALID_GEOMETRY_FACTOR = "Geometry factor (n) must be 1 (slab), 2 (cylinder), or 3 (sphere)." +ERROR_INVALID_GEOMETRY_FACTOR = ( + "Geometry factor (n) must be 1 (slab), 2 (cylinder), or 3 (sphere)." +) diff --git a/drux/params.py b/drux/params.py index d17d222..d3408e1 100644 --- a/drux/params.py +++ b/drux/params.py @@ -2,3 +2,4 @@ """Drux parameters and constants.""" DRUX_VERSION = "0.3" + diff --git a/drux/registry.py b/drux/registry.py new file mode 100644 index 0000000..52a675b --- /dev/null +++ b/drux/registry.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +"""Centralized model registry for drug release models.""" + +from math import exp, sqrt + +from .messages import ( + ERROR_ZERO_ORDER_RELEASE_RATE, + ERROR_ZERO_ORDER_INITIAL_AMOUNT, + ERROR_FIRST_ORDER_RELEASE_RATE, + ERROR_FIRST_ORDER_INITIAL_AMOUNT, + ERROR_INVALID_DIFFUSION, + ERROR_INVALID_CONCENTRATION, + ERROR_INVALID_SOLUBILITY, + ERROR_SOLUBILITY_HIGHER_THAN_CONCENTRATION, + ERROR_RELEASABLE_AMOUNT, + ERROR_WEIBULL_SCALE_PARAMETER, + ERROR_WEIBULL_SHAPE_PARAMETER, + ERROR_INVALID_EROSION_CONSTANT, + ERROR_INVALID_INITIAL_RADIUS, + ERROR_INVALID_GEOMETRY_FACTOR, +) + + +# --------------------------------------------------------------------------- +# Internal registries +# --------------------------------------------------------------------------- +_EQUATIONS = {} +_VALIDATIONS = {} +_PARAMS = {} + + +# --------------------------------------------------------------------------- +# Registration helpers +# --------------------------------------------------------------------------- + +def equation(name): + """Register a simulation equation for the given model name. + + The decorated function must have the signature ``(p, t) -> float`` + where *p* is a parameters namespace instance. + """ + def decorator(fn): + _EQUATIONS[name] = fn + return fn + return decorator + + +def register_validation(name, rules): + """Register validation rules for the given model name. + + :param name: Model name key. + :param rules: Dict of validation rules, e.g.:: + + { + "param_name": {"check": callable, "error": str}, + ... + } + + Cross-parameter validations should include ``"cross_param": True``. + """ + _VALIDATIONS[name] = rules + + +def register_params(name, params): + """Register parameter metadata for the given model name. + + :param name: Model name key. + :param params: OrderedDict-style dict of parameter specs, e.g.:: + + { + "k0": {"type": float, "description": "...", "unit": "...", "default": None}, + ... + } + """ + _PARAMS[name] = params + + +# --------------------------------------------------------------------------- +# Public accessors +# --------------------------------------------------------------------------- + +def get_model_config(name): + """Return the full model configuration dict for *name*. + + :returns: ``{"params": ..., "equation": ..., "validation": ...}`` + :raises KeyError: if *name* is not registered. + """ + return { + "params": _PARAMS[name], + "equation": _EQUATIONS[name], + "validation": _VALIDATIONS[name], + } + + +def get_equation(name): + """Return the simulation equation for *name*.""" + return _EQUATIONS[name] + + +def get_validation(name): + """Return the validation rules for *name*.""" + return _VALIDATIONS[name] + + +def get_params(name): + """Return the parameter metadata for *name*.""" + return _PARAMS[name] + + +def list_models(): + """Return a list of registered model names.""" + return list(_EQUATIONS.keys()) + + +# --------------------------------------------------------------------------- +# Zero-order model +# --------------------------------------------------------------------------- + +register_params("zero_order", { + "k0": { + "type": float, + "description": "Zero-order release rate constant", + "unit": "mg/s", + "default": None, + }, + "M0": { + "type": float, + "description": "Initial amount of drug in the solution", + "unit": "mg", + "default": 0, + }, +}) + + +@equation("zero_order") +def zero_order(p, t): + """Zero-order kinetics: M(t) = M0 + k0 * t.""" + return p.M0 + p.k0 * t + + +register_validation("zero_order", { + "k0": {"check": lambda v: v >= 0, "error": ERROR_ZERO_ORDER_RELEASE_RATE}, + "M0": {"check": lambda v: v >= 0, "error": ERROR_ZERO_ORDER_INITIAL_AMOUNT}, +}) + + +# --------------------------------------------------------------------------- +# First-order model +# --------------------------------------------------------------------------- + +register_params("first_order", { + "k": { + "type": float, + "description": "First-order release rate constant", + "unit": "1/s", + "default": None, + }, + "M0": { + "type": float, + "description": "Entire releasable amount of drug", + "unit": "mg", + "default": 1, + }, +}) + + +@equation("first_order") +def first_order(p, t): + """First-order kinetics: M(t) = M0 * (1 - exp(-k * t)).""" + return p.M0 * (1 - exp(-p.k * t)) + + +register_validation("first_order", { + "k": {"check": lambda v: v >= 0, "error": ERROR_FIRST_ORDER_RELEASE_RATE}, + "M0": {"check": lambda v: v >= 0, "error": ERROR_FIRST_ORDER_INITIAL_AMOUNT}, +}) + + +# --------------------------------------------------------------------------- +# Higuchi model +# --------------------------------------------------------------------------- + +register_params("higuchi", { + "D": { + "type": float, + "description": "Drug diffusivity in the polymer carrier", + "unit": "cm^2/s", + "default": None, + }, + "c0": { + "type": float, + "description": "Initial drug concentration", + "unit": "mg/cm^3", + "default": None, + }, + "cs": { + "type": float, + "description": "Drug solubility in the polymer", + "unit": "mg/cm^3", + "default": None, + }, +}) + + +@equation("higuchi") +def higuchi(p, t): + """Higuchi model: M(t) = sqrt(D * (2*c0 - cs) * cs * t).""" + return sqrt(p.D * (2 * p.c0 - p.cs) * p.cs * t) + + +register_validation("higuchi", { + "D": {"check": lambda v: v > 0, "error": ERROR_INVALID_DIFFUSION}, + "c0": {"check": lambda v: v > 0, "error": ERROR_INVALID_CONCENTRATION}, + "cs": {"check": lambda v: v > 0, "error": ERROR_INVALID_SOLUBILITY}, + "cs_vs_c0": { + "check": lambda p: p.cs <= p.c0, + "error": ERROR_SOLUBILITY_HIGHER_THAN_CONCENTRATION, + "cross_param": True, + }, +}) + + +# --------------------------------------------------------------------------- +# Weibull model +# --------------------------------------------------------------------------- + +register_params("weibull", { + "M": { + "type": float, + "description": "Entire releasable amount of drug", + "unit": "mg", + "default": 1, + }, + "a": { + "type": float, + "description": "Scale factor", + "unit": "dimensionless", + "default": None, + }, + "b": { + "type": float, + "description": "Shape factor", + "unit": "dimensionless", + "default": None, + }, +}) + + +@equation("weibull") +def weibull(p, t): + """Weibull model: M(t) = M * (1 - exp(-a * t^b)).""" + return p.M * (1 - exp(-p.a * t ** p.b)) + + +register_validation("weibull", { + "M": {"check": lambda v: v >= 0, "error": ERROR_RELEASABLE_AMOUNT}, + "a": {"check": lambda v: v > 0, "error": ERROR_WEIBULL_SCALE_PARAMETER}, + "b": {"check": lambda v: v > 0, "error": ERROR_WEIBULL_SHAPE_PARAMETER}, +}) + + +# --------------------------------------------------------------------------- +# Hopfenberg model +# --------------------------------------------------------------------------- + +register_params("hopfenberg", { + "M": { + "type": float, + "description": "Entire releasable amount of drug", + "unit": "mg", + "default": 1, + }, + "k0": { + "type": float, + "description": "Erosion rate constant", + "unit": "mg/(mm^2·s)", + "default": None, + }, + "c0": { + "type": float, + "description": "Initial drug concentration in the matrix", + "unit": "mg/mm^3", + "default": None, + }, + "a0": { + "type": float, + "description": "Initial radius or half-thickness of the device", + "unit": "mm", + "default": None, + }, + "n": { + "type": "int", + "description": "Geometry factor (1=slab, 2=cylinder, 3=sphere)", + "unit": "dimensionless", + "default": None, + }, +}) + + +@equation("hopfenberg") +def hopfenberg(p, t): + """Hopfenberg model: M(t) = M * (1 - (1 - k0*t/(c0*a0))^n).""" + return p.M * (1 - (1 - (p.k0 * t) / (p.c0 * p.a0)) ** p.n) + + +register_validation("hopfenberg", { + "M": {"check": lambda v: v >= 0, "error": ERROR_RELEASABLE_AMOUNT}, + "k0": {"check": lambda v: v >= 0, "error": ERROR_INVALID_EROSION_CONSTANT}, + "c0": {"check": lambda v: v > 0, "error": ERROR_INVALID_CONCENTRATION}, + "a0": {"check": lambda v: v > 0, "error": ERROR_INVALID_INITIAL_RADIUS}, + "n": { + "check": lambda v: v in (1, 2, 3), + "error": ERROR_INVALID_GEOMETRY_FACTOR, + }, +}) diff --git a/drux/weibull.py b/drux/weibull.py index 8754df2..fc24e7f 100644 --- a/drux/weibull.py +++ b/drux/weibull.py @@ -1,72 +1,11 @@ # -*- coding: utf-8 -*- """Drux Weibull model implementation.""" -from .base_model import DrugReleaseModel -from .messages import ( - ERROR_WEIBULL_SCALE_PARAMETER, - ERROR_RELEASABLE_AMOUNT, - ERROR_WEIBULL_SHAPE_PARAMETER, -) -from dataclasses import dataclass -from math import exp - - -@dataclass -class WeibullParameters: - """ - Parameters for the Weibull model based on physical formulation. - - Attributes: - M (float): entire releasable amount of drug (normally M > 0) (mg) - a (float): scale factor - b (float): shape factor - """ - - M: float - a: float - b: float - - -class WeibullModel(DrugReleaseModel): - """Simulator for the Weibull drug release model using analytical expressions based on concentration conditions.""" - - def __init__(self, M: float, a: float, b: float) -> None: - """ - Initialize the Weibull model with the given parameters. +from .base_model import create_model_class - :param M: entire releasable amount of drug (normally M > 0) (mg) - :param a: scale factor - :param b: shape factor - """ - super().__init__() - self._parameters = WeibullParameters(M=M, a=a, b=b) - self._plot_parameters["label"] = "Weibull Model" - - def __repr__(self): - """Return a string representation of the Weibull model.""" - return f"drux.WeibullModel(M={self._parameters.M}, a={self._parameters.a}, b={self._parameters.b})" - - def _model_function(self, t: float) -> float: - """ - Calculate the drug release at time t using the Weibull model. - - Formula: - - General case: Mt = M * (1 - exp(-a*t ** b)) - :param t: time (s) - """ - M = self._parameters.M - a = self._parameters.a - b = self._parameters.b - - Mt = M * (1 - exp(-a * t**b)) - - return Mt - - def _validate_parameters(self) -> None: - """Validate the parameters of the Weibull model.""" - if self._parameters.M < 0: - raise ValueError(ERROR_RELEASABLE_AMOUNT) - if self._parameters.a <= 0: - raise ValueError(ERROR_WEIBULL_SCALE_PARAMETER) - if self._parameters.b <= 0: - raise ValueError(ERROR_WEIBULL_SHAPE_PARAMETER) +WeibullModel = create_model_class( + model_name="weibull", + class_name="WeibullModel", + label="Weibull Model", + docstring="Simulator for the Weibull drug release model using analytical expressions based on concentration conditions.", +) diff --git a/drux/zero_order.py b/drux/zero_order.py index 57ac1e0..7b02c6f 100644 --- a/drux/zero_order.py +++ b/drux/zero_order.py @@ -1,61 +1,11 @@ # -*- coding: utf-8 -*- """Drux zero-order model implementation.""" -from .base_model import DrugReleaseModel -from .messages import ERROR_ZERO_ORDER_RELEASE_RATE, ERROR_ZERO_ORDER_INITIAL_AMOUNT -from dataclasses import dataclass - - -@dataclass -class ZeroOrderParameters: - """ - Parameters for the Zero-order model. - - Attributes: - M0 (float): initial amount of drug in the solution (most times, M0 = 0) - k0 (float): zero-order release rate constant (mg/s) - """ - - M0: float - k0: float - - -class ZeroOrderModel(DrugReleaseModel): - """Simulator for the Zero-order drug release model.""" - - def __init__(self, k0: float, M0: float = 0) -> None: - """ - Initialize the zero-order model with the given parameters. - - :param k0: Zero-order release rate constant (mg/s) - :param M0: Initial amount of drug in the solution (mg), default is 0 - """ - super().__init__() - self._parameters = ZeroOrderParameters(k0=k0, M0=M0) - self._plot_parameters["label"] = "Zero-Order Model" - - def __repr__(self): - """Return a string representation of the Zero-Order model.""" - return f"drux.ZeroOrderModel(k0={self._parameters.k0}, M0={self._parameters.M0})" - - def _model_function(self, t: float) -> float: - """ - Calculate the drug release at time t using the zero-order model. - - Formula: - - M(t) = M0 + k0 * t - :param t: time (s) - """ - M0 = self._parameters.M0 - k0 = self._parameters.k0 - - Mt = M0 + k0 * t - - return Mt - - def _validate_parameters(self) -> None: - """Validate the parameters of the zero-order model.""" - if self._parameters.M0 < 0: - raise ValueError(ERROR_ZERO_ORDER_INITIAL_AMOUNT) - if self._parameters.k0 < 0: - raise ValueError(ERROR_ZERO_ORDER_RELEASE_RATE) +from .base_model import create_model_class + +ZeroOrderModel = create_model_class( + model_name="zero_order", + class_name="ZeroOrderModel", + label="Zero-Order Model", + docstring="Simulator for the zero-order drug release model.", +)