Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install pandas && pip install -e ".[dev]"
- run: pip install pandas && pip install -e ".[dev,api,viz]"
- name: Run tests with coverage
run: |
pytest tests/ -m "not slow and not benchmark" \
Expand All @@ -58,7 +58,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install pandas && pip install -e ".[dev]"
- run: pip install pandas && pip install -e ".[dev,api,viz]"
- name: CLI smoke
run: omega --help
- name: Import smoke
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ viz = ["matplotlib~=3.7"]
dev = [
"pytest~=8.0",
"pytest-cov~=5.0",
"pytest-timeout>=2.2",
"ruff~=0.6",
"mypy~=1.8",
"types-PyYAML",
Expand Down
2 changes: 1 addition & 1 deletion src/omega_pbpk/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from omega_pbpk.adapters.yaml_loader import load_drug_spec, drug_to_spec, spec_to_drug
from omega_pbpk.adapters.yaml_loader import drug_to_spec, load_drug_spec, spec_to_drug

__all__ = ["load_drug_spec", "drug_to_spec", "spec_to_drug"]
1 change: 1 addition & 0 deletions src/omega_pbpk/adapters/population_adapter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""VirtualPopulation → PatientSpec 변환 어댑터."""

from __future__ import annotations

from omega_pbpk.contracts.patient_spec import PatientSpec
Expand Down
15 changes: 9 additions & 6 deletions src/omega_pbpk/adapters/yaml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
spec = load_drug_spec("compounds/caffeine.yaml")
spec.validate()
"""

from __future__ import annotations

import warnings
from pathlib import Path
from typing import Literal

Expand All @@ -34,9 +34,7 @@

def drug_to_spec(drug: Drug, param_source: str = "yaml") -> DrugSpec:
"""기존 Drug 객체 → DrugSpec 변환."""
compound_type = _DRUG_TYPE_MAP.get(
getattr(drug, "drug_type", "neutral"), "neutral"
)
compound_type = _DRUG_TYPE_MAP.get(getattr(drug, "drug_type", "neutral"), "neutral")
return DrugSpec(
name=drug.name,
smiles=drug.smiles,
Expand All @@ -61,8 +59,12 @@ def drug_to_spec(drug: Drug, param_source: str = "yaml") -> DrugSpec:
def spec_to_drug(spec: DrugSpec) -> Drug:
"""DrugSpec → 기존 Drug 객체 역변환 (ODE 엔진 호환용)."""
# compound_type → drug_type 역매핑
_reverse_map = {"neutral": "neutral", "acid": "monoprotic_acid",
"base": "monoprotic_base", "zwitterion": "diprotic"}
_reverse_map = {
"neutral": "neutral",
"acid": "monoprotic_acid",
"base": "monoprotic_base",
"zwitterion": "diprotic",
}
return Drug(
name=spec.name,
smiles=spec.smiles,
Expand Down Expand Up @@ -96,6 +98,7 @@ def load_drug_spec(path: str | Path) -> DrugSpec:
FileNotFoundError: YAML 파일 없음
"""
from omega_pbpk.config import load_compound

drug = load_compound(path)
return drug_to_spec(drug, param_source="yaml")

Expand Down
11 changes: 7 additions & 4 deletions src/omega_pbpk/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
try:
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse

HAS_FASTAPI = True
except ImportError:
HAS_FASTAPI = False
Expand All @@ -37,8 +38,7 @@

if not HAS_FASTAPI:
raise ImportError(
"FastAPI is required to use the API server. "
"Install with: pip install omega-pbpk[api]"
"FastAPI is required to use the API server. Install with: pip install omega-pbpk[api]"
)

app = FastAPI(
Expand Down Expand Up @@ -653,13 +653,14 @@ def train_surrogate(req: TrainSurrogateRequest) -> TrainSurrogateResponse:
if req.n_samples < 10 or req.n_samples > 10000:
raise HTTPException(status_code=422, detail="n_samples must be 10–10000")
try:
from omega_pbpk.surrogate.data_generator import generate_training_data
from omega_pbpk.surrogate import PKSurrogate
from omega_pbpk.surrogate.data_generator import generate_training_data

data = generate_training_data(n_samples=min(req.n_samples, 100)) # MVP: 100으로 제한
model = PKSurrogate(n_input=data.n_params)
model.train(data.X, data.y, epochs=min(req.epochs, 20))
import os

os.makedirs(req.output_dir, exist_ok=True)
model.save(req.output_dir)
return TrainSurrogateResponse(
Expand Down Expand Up @@ -695,6 +696,8 @@ def validate(req: ValidateRequest) -> ValidateResponse:
except Exception as exc:
return ValidateResponse(mode="benchmark", passed=False, results={"error": str(exc)})
elif req.mode == "sanity":
return ValidateResponse(mode="sanity", passed=True, results={"message": "Sanity checks passed"})
return ValidateResponse(
mode="sanity", passed=True, results={"message": "Sanity checks passed"}
)
else:
raise HTTPException(status_code=422, detail=f"Unknown mode: {req.mode}")
1 change: 1 addition & 0 deletions src/omega_pbpk/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This module is kept for backward compatibility.
"""

import warnings

warnings.warn(
Expand Down
24 changes: 12 additions & 12 deletions src/omega_pbpk/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@
# 4-verb sub-apps (M3 CLI restructure)
# ---------------------------------------------------------------------------
simulate_app = typer.Typer(help="Run PBPK simulations.")
train_app = typer.Typer(help="Train ML surrogate or calibrate models.")
train_app = typer.Typer(help="Train ML surrogate or calibrate models.")
validate_app = typer.Typer(help="Validate, benchmark, and QA.")
serve_app = typer.Typer(help="Start API server.")
serve_app = typer.Typer(help="Start API server.")

app.add_typer(simulate_app, name="simulate")
app.add_typer(train_app, name="train")
app.add_typer(train_app, name="train")
app.add_typer(validate_app, name="validate")
app.add_typer(serve_app, name="serve")
app.add_typer(serve_app, name="serve")

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("omega_pbpk")
Expand Down Expand Up @@ -1408,10 +1408,8 @@ def serve(
try:
import uvicorn
except ImportError:
typer.echo(
"API server requires [api] extras. Install with: pip install omega-pbpk[api]"
)
raise SystemExit(1)
typer.echo("API server requires [api] extras. Install with: pip install omega-pbpk[api]")
raise SystemExit(1) from None

typer.echo(f"Starting Omega PBPK API server on http://{host}:{port}")
uvicorn.run("omega_pbpk.api.app:app", host=host, port=port, reload=reload)
Expand All @@ -1435,6 +1433,7 @@ def run_tests() -> None:

# --- simulate group ----------------------------------------------------------


@simulate_app.command("single")
def simulate_single(
compound: str = typer.Argument(..., help="Compound name or YAML path"),
Expand Down Expand Up @@ -1529,6 +1528,7 @@ def simulate_pgx(

# --- train group -------------------------------------------------------------


@train_app.command("surrogate")
def train_surrogate_cmd(
n_samples: int = typer.Option(500, help="Training samples"),
Expand Down Expand Up @@ -1572,6 +1572,7 @@ def train_calibrate_cmd(

# --- validate group ----------------------------------------------------------


@validate_app.command("benchmark")
def validate_benchmark_cmd(
suite_dir: str = typer.Option("benchmarks", help="Path to benchmark suite directory."),
Expand Down Expand Up @@ -1629,6 +1630,7 @@ def validate_sensitivity_cmd(

# --- serve group -------------------------------------------------------------


@serve_app.command("start")
def serve_start(
host: str = typer.Option("0.0.0.0", help="Host to bind the server to."),
Expand All @@ -1639,10 +1641,8 @@ def serve_start(
try:
import uvicorn
except ImportError:
typer.echo(
"API server requires [api] extras: pip install omega-pbpk[api]", err=True
)
raise typer.Exit(1)
typer.echo("API server requires [api] extras: pip install omega-pbpk[api]", err=True)
raise typer.Exit(1) from None

typer.echo(f"Starting Omega PBPK API server on http://{host}:{port}")
uvicorn.run("omega_pbpk.api.app:app", host=host, port=port, reload=reload)
Expand Down
11 changes: 7 additions & 4 deletions src/omega_pbpk/contracts/drug_spec.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal

Expand All @@ -11,12 +12,12 @@ class DrugSpec:
logP: float = 2.0
pka: list[float] = field(default_factory=lambda: [7.0])
compound_type: Literal["neutral", "acid", "base", "zwitterion"] = "neutral"
fup: float = 0.5 # fraction unbound in plasma
rbp: float = 1.0 # blood:plasma ratio
fup: float = 0.5 # fraction unbound in plasma
rbp: float = 1.0 # blood:plasma ratio
clint_hepatic_L_per_h: float = 0.0
clint_gut_L_per_h: float = 0.0
clr_L_per_h: float = 0.0
peff: float = 1.0 # effective permeability (cm/s × 10^-4)
peff: float = 1.0 # effective permeability (cm/s × 10^-4)
solubility_mg_mL: float = 1.0
kp: dict[str, float] = field(default_factory=dict)
permeability_limited: dict[str, dict[str, float]] = field(default_factory=dict)
Expand All @@ -32,7 +33,9 @@ def validate(self) -> None:
if self.mw <= 0:
raise ValueError(f"mw must be > 0, got {self.mw}")
if self.clint_hepatic_L_per_h < 0:
raise ValueError(f"clint_hepatic_L_per_h must be >= 0, got {self.clint_hepatic_L_per_h}")
raise ValueError(
f"clint_hepatic_L_per_h must be >= 0, got {self.clint_hepatic_L_per_h}"
)
if self.clr_L_per_h < 0:
raise ValueError(f"clr_L_per_h must be >= 0, got {self.clr_L_per_h}")
if self.peff < 0:
Expand Down
3 changes: 2 additions & 1 deletion src/omega_pbpk/contracts/patient_spec.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

Expand All @@ -12,7 +13,7 @@ class PatientSpec:
gfr_mL_min: float = 125.0
cardiac_output_L_h: float = 390.0
child_pugh: Literal["normal", "A", "B", "C"] = "normal"
cyp3a4_activity: float = 1.0 # relative to EM
cyp3a4_activity: float = 1.0 # relative to EM
cyp2d6_activity: float = 1.0
cyp2c9_activity: float = 1.0
hepatic_cl_factor: float = 1.0
Expand Down
12 changes: 7 additions & 5 deletions src/omega_pbpk/contracts/simulation_io.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal

import numpy as np
from numpy.typing import NDArray

Expand Down Expand Up @@ -38,12 +40,12 @@ class PKMetrics:

@dataclass(frozen=True)
class ADMEOutput:
Fa: float = 1.0 # fraction absorbed
Fg: float = 1.0 # fraction surviving gut
Fh: float = 1.0 # fraction surviving liver
Fa: float = 1.0 # fraction absorbed
Fg: float = 1.0 # fraction surviving gut
Fh: float = 1.0 # fraction surviving liver
CLint: float = 0.0 # intrinsic clearance L/h
fu: float = 0.5 # fraction unbound
Vd: float = 30.0 # volume of distribution L
fu: float = 0.5 # fraction unbound
Vd: float = 30.0 # volume of distribution L
confidence: Literal["low", "medium", "high"] = "low"


Expand Down
2 changes: 1 addition & 1 deletion src/omega_pbpk/engine/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SimulationResult:
"""ODE 시뮬레이션 결과."""

t: NDArray[np.float64]
amounts: NDArray[np.float64] # shape (n_states, n_timepoints)
amounts: NDArray[np.float64] # shape (n_states, n_timepoints)
plasma_concentration: NDArray[np.float64] # shape (n_timepoints,)
drug_name: str = ""
route: str = "oral"
Expand Down
2 changes: 1 addition & 1 deletion src/omega_pbpk/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from omega_pbpk.plugins.base import PluginBase, SurrogateModelPlugin
from omega_pbpk.plugins.adme_plugin import ADMEPredictorPlugin
from omega_pbpk.plugins.base import PluginBase, SurrogateModelPlugin
from omega_pbpk.plugins.heuristic_kp import HeuristicKpPlugin
from omega_pbpk.plugins.parameter_net import ParameterNetPlugin

Expand Down
19 changes: 11 additions & 8 deletions src/omega_pbpk/plugins/adme_plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from omega_pbpk.plugins.base import PluginBase
from omega_pbpk.contracts import DrugSpec
from omega_pbpk.plugins.base import PluginBase


class ADMEPredictorPlugin(PluginBase):
Expand All @@ -12,6 +12,7 @@ class ADMEPredictorPlugin(PluginBase):

def __init__(self) -> None:
from omega_pbpk.prediction.adme_predictor import ADMEPredictor

self._predictor = ADMEPredictor()

def predict(self, spec: DrugSpec) -> dict[str, float | dict]:
Expand All @@ -21,13 +22,15 @@ def predict(self, spec: DrugSpec) -> dict[str, float | dict]:
props = self._predictor.predict(smiles)
else:
# SMILES 없을 때: DrugSpec의 physicochemical 값으로 fallback
props = self._predictor.predict_from_dict({
"mw": spec.mw,
"logP": spec.logP,
"fup": spec.fup,
"rbp": spec.rbp,
"peff": spec.peff,
})
props = self._predictor.predict_from_dict(
{
"mw": spec.mw,
"logP": spec.logP,
"fup": spec.fup,
"rbp": spec.rbp,
"peff": spec.peff,
}
)

return {
"fup": float(props.fup),
Expand Down
10 changes: 5 additions & 5 deletions src/omega_pbpk/plugins/heuristic_kp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from typing import Literal

from omega_pbpk.plugins.base import PluginBase
from omega_pbpk.contracts import DrugSpec
from omega_pbpk.plugins.base import PluginBase


class HeuristicKpPlugin(PluginBase):
Expand All @@ -12,16 +12,15 @@ class HeuristicKpPlugin(PluginBase):
name = "heuristic_kp"
provides = frozenset({"kp"})

def __init__(
self, method: Literal["poulin_theil", "rodgers_rowland"] = "poulin_theil"
) -> None:
def __init__(self, method: Literal["poulin_theil", "rodgers_rowland"] = "poulin_theil") -> None:
self.method = method

def predict(self, spec: DrugSpec) -> dict[str, float | dict]:
pka = spec.pka[0] if spec.pka else None

if self.method == "rodgers_rowland":
from omega_pbpk.core.heuristics import rodgers_rowland_kp, _TISSUE_FACTORS
from omega_pbpk.core.heuristics import _TISSUE_FACTORS, rodgers_rowland_kp

kp_dict: dict[str, float] = {
tissue: rodgers_rowland_kp(
logP=spec.logP,
Expand All @@ -35,6 +34,7 @@ def predict(self, spec: DrugSpec) -> dict[str, float | dict]:
else:
# poulin_theil (default)
from omega_pbpk.core.heuristics import estimate_all_kp

kp_dict = estimate_all_kp(
logP=spec.logP,
pka=pka,
Expand Down
1 change: 1 addition & 0 deletions src/omega_pbpk/plugins/parameter_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
MVP 구현: PGx CYP scaling + 체중/연령 allometric scaling.
추후 ML NN으로 교체 가능한 PluginBase 구조.
"""

from __future__ import annotations

from omega_pbpk.contracts.drug_spec import DrugSpec
Expand Down
Loading
Loading