From 4c837fd2c709581ab301a8c8198f83dd76085d8d Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:17:30 +0100 Subject: [PATCH 01/16] centralize model registries --- drux/params.py | 172 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/drux/params.py b/drux/params.py index d17d222..fd2e51c 100644 --- a/drux/params.py +++ b/drux/params.py @@ -1,4 +1,176 @@ # -*- coding: utf-8 -*- """Drux parameters and constants.""" +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, +) + DRUX_VERSION = "0.3" + +MODELS_REGISTRY = { + "zero_order": { + "params": { + "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": lambda p, t: p.M0 + p.k0 * t, + "validation": { + "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": { + "params": { + "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": lambda p, t: p.M0 * (1 - exp(-p.k * t)), + "validation": { + "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": { + "params": { + "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": lambda p, t: sqrt(p.D * (2 * p.c0 - p.cs) * p.cs * t), + "validation": { + "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": { + "params": { + "a": { + "type": "float", + "description": "Scale factor", + "unit": "dimensionless", + "default": None, + }, + "b": { + "type": "float", + "description": "Shape factor", + "unit": "dimensionless", + "default": None, + }, + "M": { + "type": "float", + "description": "Entire releasable amount of drug", + "unit": "mg", + "default": 1, + }, + }, + "equation": lambda p, t: p.M * (1 - exp(-p.a * t**p.b)), + "validation": { + "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": { + "params": { + "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, + }, + "M": { + "type": "float", + "description": "Entire releasable amount of drug", + "unit": "mg", + "default": 1, + }, + }, + "equation": lambda p, t: p.M * (1 - (1 - (p.k0 * t) / (p.c0 * p.a0)) ** p.n), + "validation": { + "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, + }, + }, + }, +} From 9b0916270c67092c395fdbd1436f7b761e2c12cd Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:18:35 +0100 Subject: [PATCH 02/16] use model registry instead of abstract methods --- drux/base_model.py | 57 +++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/drux/base_model.py b/drux/base_model.py index 2eee8fa..dc96169 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 abc import ABC from typing import Any, Optional from .messages import ( @@ -14,6 +14,8 @@ ERROR_TARGET_RELEASE_EXCEEDS_MAX, ) +from .params import MODELS_REGISTRY + class DrugReleaseModel(ABC): """ @@ -35,25 +37,44 @@ 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. + Validate model parameters using MODELS_REGISTRY. - Should raise ValueError if parameters are invalid. + Raises ValueError if parameters are invalid. """ - pass + if self._model_name is None or self._parameters is None: + raise ValueError("Model name and parameters must be set") + + config = MODELS_REGISTRY[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 = MODELS_REGISTRY[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 +112,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 +134,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"]) From f36a09a9e43bad15d796ed20c6d5689a47856a70 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:19:45 +0100 Subject: [PATCH 03/16] method to create params dataclass from model registry --- drux/utils.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 drux/utils.py diff --git a/drux/utils.py b/drux/utils.py new file mode 100644 index 0000000..c700835 --- /dev/null +++ b/drux/utils.py @@ -0,0 +1,25 @@ +from dataclasses import field, make_dataclass +from .params import MODELS_REGISTRY + + +def create_parameters_dataclass(model_name: str): + """ + Create a dataclass from MODELS_REGISTRY configuration. + + :param model_name: Name of the model in MODELS_REGISTRY + :return: Dataclass type for model parameters + """ + config = MODELS_REGISTRY[model_name] + fields = [] + + for param_name, param_info in config["params"].items(): + param_type = eval(param_info["type"]) + default_value = param_info["default"] + + if default_value is not None: + fields.append((param_name, param_type, field(default=default_value))) + else: + fields.append((param_name, param_type)) + + class_name = f"{model_name.title().replace('_', '')}Parameters" + return make_dataclass(class_name, fields) From fdcf9a78593c141143c2518b05a263b4160e40d3 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:20:02 +0100 Subject: [PATCH 04/16] remove models dataclasses --- drux/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drux/__init__.py b/drux/__init__.py index e6788d6..29fb78f 100644 --- a/drux/__init__.py +++ b/drux/__init__.py @@ -2,10 +2,10 @@ """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 +from .higuchi import HiguchiModel +from .zero_order import ZeroOrderModel +from .first_order import FirstOrderModel +from .weibull import WeibullModel +from .hopfenberg import HopfenbergModel __version__ = DRUX_VERSION From f46bc28801cae444c054cf215076e830ac9193f6 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:20:20 +0100 Subject: [PATCH 05/16] pep8 refactor --- drux/messages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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)." +) From 9f38e5e94dc9277687f76e087226c8aac2c07ef0 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:01 +0100 Subject: [PATCH 06/16] update with central model registry --- drux/zero_order.py | 57 +++++++--------------------------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/drux/zero_order.py b/drux/zero_order.py index 57ac1e0..bc52664 100644 --- a/drux/zero_order.py +++ b/drux/zero_order.py @@ -2,60 +2,19 @@ """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 +from .utils import create_parameters_dataclass 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 - """ + def __init__(self, k0: float, M0: float = 0): super().__init__() - self._parameters = ZeroOrderParameters(k0=k0, M0=M0) + self._model_name = "zero_order" + _params_class = create_parameters_dataclass(self._model_name) + self._parameters = _params_class(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) + return ( + f"drux.ZeroOrderModel(k0={self._parameters.k0}, M0={self._parameters.M0})" + ) From 4f63703e1318c74de224a81010f20f1099df8813 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:10 +0100 Subject: [PATCH 07/16] update with central model registry --- drux/first_order.py | 46 +++++---------------------------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/drux/first_order.py b/drux/first_order.py index 40e6215..ec245dd 100644 --- a/drux/first_order.py +++ b/drux/first_order.py @@ -1,29 +1,13 @@ # -*- 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 - - -@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 +from .utils import create_parameters_dataclass class FirstOrderModel(DrugReleaseModel): """Simulator for the first-order drug release model.""" - def __init__(self, k: float, M0: float) -> None: + def __init__(self, k: float, M0: float = 1) -> None: """ Initialize the first-order model with the given parameters. @@ -31,31 +15,11 @@ def __init__(self, k: float, M0: float) -> None: :param M0: entire releasable amount of drug (the asymptotic maximum) (mg) """ super().__init__() - self._parameters = FirstOrderParameters(k=k, M0=M0) + self._model_name = "first_order" + _params_class = create_parameters_dataclass(self._model_name) + self._parameters = _params_class(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) From ca063fc3e5dd87b3c907dec7aecbb71669c96b42 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:18 +0100 Subject: [PATCH 08/16] update with central model registry --- drux/higuchi.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/drux/higuchi.py b/drux/higuchi.py index 9df2fbf..75bc324 100644 --- a/drux/higuchi.py +++ b/drux/higuchi.py @@ -2,30 +2,7 @@ """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 +from .utils import create_parameters_dataclass class HiguchiModel(DrugReleaseModel): From a5e3b7fd758fbd9b90e7cff35d6432ce37ce2d1f Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:29 +0100 Subject: [PATCH 09/16] update with central model registry --- drux/higuchi.py | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/drux/higuchi.py b/drux/higuchi.py index 75bc324..bb7722e 100644 --- a/drux/higuchi.py +++ b/drux/higuchi.py @@ -17,7 +17,9 @@ def __init__(self, D: float, c0: float, cs: float) -> None: :param cs: Drug solubility in the polymer (mg/cm^3) """ super().__init__() - self._parameters = HiguchiParameters(D=D, c0=c0, cs=cs) + self._model_name = "higuchi" + _params_class = create_parameters_dataclass(self._model_name) + self._parameters = _params_class(D=D, c0=c0, cs=cs) self._plot_parameters["label"] = "Higuchi Model" def __repr__(self): @@ -26,30 +28,3 @@ def __repr__(self): 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) From e4972c36dfb65fd5a3bb689155cffaf1217f1814 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:35 +0100 Subject: [PATCH 10/16] update with central model registry --- drux/hopfenberg.py | 69 ++++------------------------------------------ 1 file changed, 5 insertions(+), 64 deletions(-) diff --git a/drux/hopfenberg.py b/drux/hopfenberg.py index 89074f9..f8e2495 100644 --- a/drux/hopfenberg.py +++ b/drux/hopfenberg.py @@ -2,39 +2,13 @@ """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 +from .utils import create_parameters_dataclass 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: + def __init__(self, k0: float, c0: float, a0: float, n: int, M: float = 1) -> None: """ Initialize the Hopfenberg model with the given parameters. @@ -45,7 +19,9 @@ def __init__(self, M: float, k0: float, c0: float, a0: float, n: int) -> None: :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._model_name = "hopfenberg" + _params_class = create_parameters_dataclass(self._model_name) + self._parameters = _params_class(M=M, k0=k0, c0=c0, a0=a0, n=n) self._plot_parameters["label"] = "Hopfenberg Model" def __repr__(self): @@ -55,38 +31,3 @@ def __repr__(self): 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) From 36398fe6cb2ae31cb837bbf35290ea8034d16b4d Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:21:51 +0100 Subject: [PATCH 11/16] update with central model registry --- drux/weibull.py | 55 +++++-------------------------------------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/drux/weibull.py b/drux/weibull.py index 8754df2..e4aaa76 100644 --- a/drux/weibull.py +++ b/drux/weibull.py @@ -2,35 +2,13 @@ """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 +from .utils import create_parameters_dataclass 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: + def __init__(self, a: float, b: float, M: float = 1) -> None: """ Initialize the Weibull model with the given parameters. @@ -39,34 +17,11 @@ def __init__(self, M: float, a: float, b: float) -> None: :param b: shape factor """ super().__init__() - self._parameters = WeibullParameters(M=M, a=a, b=b) + self._model_name = "weibull" + _params_class = create_parameters_dataclass(self._model_name) + self._parameters = _params_class(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) From cdb5ef325b79def7464a14c809fff9c3e9404132 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:38:56 +0100 Subject: [PATCH 12/16] remove eval --- drux/params.py | 28 ++++++++++++++-------------- drux/utils.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drux/params.py b/drux/params.py index fd2e51c..5f2518e 100644 --- a/drux/params.py +++ b/drux/params.py @@ -25,13 +25,13 @@ "zero_order": { "params": { "k0": { - "type": "float", + "type": float, "description": "Zero-order release rate constant", "unit": "mg/s", "default": None, }, "M0": { - "type": "float", + "type": float, "description": "Initial amount of drug in the solution", "unit": "mg", "default": 0, @@ -46,13 +46,13 @@ "first_order": { "params": { "k": { - "type": "float", + "type": float, "description": "First-order release rate constant", "unit": "1/s", "default": None, }, "M0": { - "type": "float", + "type": float, "description": "Entire releasable amount of drug", "unit": "mg", "default": 1, @@ -70,19 +70,19 @@ "higuchi": { "params": { "D": { - "type": "float", + "type": float, "description": "Drug diffusivity in the polymer carrier", "unit": "cm^2/s", "default": None, }, "c0": { - "type": "float", + "type": float, "description": "Initial drug concentration", "unit": "mg/cm^3", "default": None, }, "cs": { - "type": "float", + "type": float, "description": "Drug solubility in the polymer", "unit": "mg/cm^3", "default": None, @@ -103,19 +103,19 @@ "weibull": { "params": { "a": { - "type": "float", + "type": float, "description": "Scale factor", "unit": "dimensionless", "default": None, }, "b": { - "type": "float", + "type": float, "description": "Shape factor", "unit": "dimensionless", "default": None, }, "M": { - "type": "float", + "type": float, "description": "Entire releasable amount of drug", "unit": "mg", "default": 1, @@ -131,19 +131,19 @@ "hopfenberg": { "params": { "k0": { - "type": "float", + "type": float, "description": "Erosion rate constant", "unit": "mg/(mm^2·s)", "default": None, }, "c0": { - "type": "float", + "type": float, "description": "Initial drug concentration in the matrix", "unit": "mg/mm^3", "default": None, }, "a0": { - "type": "float", + "type": float, "description": "Initial radius or half-thickness of the device", "unit": "mm", "default": None, @@ -155,7 +155,7 @@ "default": None, }, "M": { - "type": "float", + "type": float, "description": "Entire releasable amount of drug", "unit": "mg", "default": 1, diff --git a/drux/utils.py b/drux/utils.py index c700835..afa1325 100644 --- a/drux/utils.py +++ b/drux/utils.py @@ -13,7 +13,7 @@ def create_parameters_dataclass(model_name: str): fields = [] for param_name, param_info in config["params"].items(): - param_type = eval(param_info["type"]) + param_type = param_info["type"] default_value = param_info["default"] if default_value is not None: From a59125d6d66d515ed94234622c9f6ecccc24fec1 Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:41:25 +0100 Subject: [PATCH 13/16] add file docstring --- drux/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drux/utils.py b/drux/utils.py index afa1325..2672e8d 100644 --- a/drux/utils.py +++ b/drux/utils.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +"""Generic utilities for Drux.""" + from dataclasses import field, make_dataclass from .params import MODELS_REGISTRY From 99d620bc5356c738c689048395f33bd18857140b Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:45:35 +0100 Subject: [PATCH 14/16] add docstring --- drux/zero_order.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drux/zero_order.py b/drux/zero_order.py index bc52664..7e669ef 100644 --- a/drux/zero_order.py +++ b/drux/zero_order.py @@ -7,6 +7,12 @@ class ZeroOrderModel(DrugReleaseModel): def __init__(self, k0: float, M0: float = 0): + """ + Initialize the Zero-Order model with the given parameters. + + :param k0: Zero-order release rate constant (amount/time) + :param M0: Initial amount of drug released at time zero (default is 0 + """ super().__init__() self._model_name = "zero_order" _params_class = create_parameters_dataclass(self._model_name) From 5060639185eda3a82668979f791fbf14f3e39f6b Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:50:12 +0100 Subject: [PATCH 15/16] add docstring --- drux/zero_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drux/zero_order.py b/drux/zero_order.py index 7e669ef..028fbe3 100644 --- a/drux/zero_order.py +++ b/drux/zero_order.py @@ -6,6 +6,8 @@ class ZeroOrderModel(DrugReleaseModel): + """Simulator for the zero-order drug release model.""" + def __init__(self, k0: float, M0: float = 0): """ Initialize the Zero-Order model with the given parameters. From 92448a583282fffcb6769584999252094dbc539e Mon Sep 17 00:00:00 2001 From: alirezazolanvari Date: Tue, 24 Feb 2026 12:57:03 +0100 Subject: [PATCH 16/16] minor edit --- drux/zero_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drux/zero_order.py b/drux/zero_order.py index 028fbe3..95f7a73 100644 --- a/drux/zero_order.py +++ b/drux/zero_order.py @@ -7,7 +7,7 @@ class ZeroOrderModel(DrugReleaseModel): """Simulator for the zero-order drug release model.""" - + def __init__(self, k0: float, M0: float = 0): """ Initialize the Zero-Order model with the given parameters.