Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4c837fd
centralize model registries
alirezazolanvari Feb 24, 2026
9b09162
use model registry instead of abstract methods
alirezazolanvari Feb 24, 2026
f36a09a
method to create params dataclass from model registry
alirezazolanvari Feb 24, 2026
fdcf9a7
remove models dataclasses
alirezazolanvari Feb 24, 2026
f46bc28
pep8 refactor
alirezazolanvari Feb 24, 2026
9f38e5e
update with central model registry
alirezazolanvari Feb 24, 2026
4f63703
update with central model registry
alirezazolanvari Feb 24, 2026
ca063fc
update with central model registry
alirezazolanvari Feb 24, 2026
a5e3b7f
update with central model registry
alirezazolanvari Feb 24, 2026
e4972c3
update with central model registry
alirezazolanvari Feb 24, 2026
36398fe
update with central model registry
alirezazolanvari Feb 24, 2026
cdb5ef3
remove eval
alirezazolanvari Feb 24, 2026
a59125d
add file docstring
alirezazolanvari Feb 24, 2026
99d620b
add docstring
alirezazolanvari Feb 24, 2026
5060639
add docstring
alirezazolanvari Feb 24, 2026
92448a5
minor edit
alirezazolanvari Feb 24, 2026
2991482
centralized equation and validation registry
alirezazolanvari May 14, 2026
2769eae
use equation and validation registries
alirezazolanvari May 14, 2026
d139460
split params into required and optional
alirezazolanvari May 14, 2026
70a8fa5
add create_model_class func
alirezazolanvari May 14, 2026
4bf1a94
use create_model_class
alirezazolanvari May 14, 2026
23ee121
use create_model_class
alirezazolanvari May 14, 2026
de6d379
use create_model_class
alirezazolanvari May 14, 2026
f12b233
use create_model_class
alirezazolanvari May 14, 2026
03a95ed
use create_model_class
alirezazolanvari May 14, 2026
6bc46a6
minor fixes
alirezazolanvari May 14, 2026
4251f2f
minor change in dataclass factory
alirezazolanvari May 14, 2026
da15f99
Add registry by merging params and equations
alirezazolanvari May 14, 2026
18dc49e
hardcode versions here
alirezazolanvari May 14, 2026
5b30bd7
merge utils into base_model
alirezazolanvari May 14, 2026
29510e1
remove unused files
alirezazolanvari May 14, 2026
6dc9537
revert params for version
alirezazolanvari May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions drux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

128 changes: 103 additions & 25 deletions drux/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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):
Expand All @@ -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."""
Expand Down Expand Up @@ -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.

Expand All @@ -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"])
Expand Down Expand Up @@ -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
64 changes: 7 additions & 57 deletions drux/first_order.py
Original file line number Diff line number Diff line change
@@ -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.",
)
81 changes: 7 additions & 74 deletions drux/higuchi.py
Original file line number Diff line number Diff line change
@@ -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.",
)
Loading
Loading