Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions drux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
57 changes: 41 additions & 16 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 abc import ABC
from typing import Any, Optional

from .messages import (
Expand All @@ -14,6 +14,8 @@
ERROR_TARGET_RELEASE_EXCEEDS_MAX,
)

from .params import MODELS_REGISTRY


class DrugReleaseModel(ABC):
"""
Expand All @@ -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."""
Expand Down Expand Up @@ -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.

Expand All @@ -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"])
Expand Down
46 changes: 5 additions & 41 deletions drux/first_order.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,25 @@
# -*- 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.

: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._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)
56 changes: 4 additions & 52 deletions drux/higuchi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -40,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):
Expand All @@ -49,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)
69 changes: 5 additions & 64 deletions drux/hopfenberg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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):
Expand All @@ -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)
8 changes: 4 additions & 4 deletions drux/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."
)
Loading