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
29 changes: 29 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.git
.github
.idea
.vscode
.venv
venv
env
.pytest_cache
.mypy_cache
.ruff_cache
htmlcov
__pycache__
*.pyc
*.pyo
*.pyd
.DS_Store
Thumbs.db
Simulacoes
Tuning_NeuralNetwork
docs
tests
*.md
!README.md
*.log
*.tmp
build
dist
*.egg-info
.claude
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.{yml,yaml,toml,json}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CI

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install ruff
run: pip install "ruff>=0.5"
- name: Ruff check
run: ruff check --output-format=github .
- name: Ruff format --check
run: ruff format --check .

typecheck:
name: Type Check (core + services)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
pip install -e .[dev]
- name: mypy
run: mypy

test:
name: Tests (Py ${{ matrix.python-version }} / ${{ matrix.os }})
needs: [lint]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install
run: |
pip install -e .[dev]
- name: Run tests (sem slow)
run: pytest -m "not slow" --cov=CSTRChemIA/core --cov=CSTRChemIA/services --cov-report=xml
- name: Upload coverage (Ubuntu 3.12 only)
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: false

docker:
name: Docker build
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build image (cache)
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: cstrchemia:ci
cache-from: type=gha
cache-to: type=gha,mode=max
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,26 @@ Tuning_NeuralNetwork/
# OS
.DS_Store
Thumbs.db

# Build
build/
dist/
*.egg-info/

# Ferramentas
.ruff_cache/
.mypy_cache/
.coverage
htmlcov/
coverage.xml
.nox/
.tox/

# Docker
*.local.env

# Locks gerados
requirements.lock.txt

# Claude Code working directory
.claude/
30 changes: 30 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
exclude: \.md$
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-files
args: ["--maxkb=2048"]
- id: check-merge-conflict
- id: debug-statements
- id: mixed-line-ending
args: ["--fix=lf"]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.5
hooks:
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix"]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: ["numpy", "pandas"]
files: ^CSTRChemIA/(core|services)/
args: ["--config-file=pyproject.toml"]
20 changes: 20 additions & 0 deletions CSTRChemIA/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CSTRChemIA (package)

Veja o [README principal](../README.md) para visão geral, instalação,
arquitetura e deployment.

Este subdiretório contém o código do pacote Python. Estrutura:

```
core/ # Infra transversal (logging, settings, constants, ...)
services/ # Camada de aplicação (fachadas)
api/ # REST API FastAPI (opcional)
apps/ # Callbacks e init do Dash
layouts/ # Layouts das abas
assets/ # CSS / logo
surrogate_model/ # Modelos Keras + scalers
optimization/ # NSGA-II
training/ # Pipeline de treino
simulation/ # Simulador CSTR (ODEs)
main.py # Entry-point Dash (+ /healthz)
```
10 changes: 10 additions & 0 deletions CSTRChemIA/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
api — REST API opcional (FastAPI) sobre os services.

Sobe separado do Dash para permitir consumo programático dos modelos
surrogate e do NSGA-II. Requer extras ``cstrchemia[api]``.
"""

from api.app import create_app

__all__ = ["create_app"]
157 changes: 157 additions & 0 deletions CSTRChemIA/api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
FastAPI app sobre os services do CSTRChemIA.

Endpoints:
GET /health — status + modelos disponíveis
GET /models — lista de modelos com métricas
POST /predict — predição unitária
POST /optimize — dispara NSGA-II (async)
GET /jobs/{id} — status do job de otimização
GET / — redirect para /docs

Uso em dev:
uvicorn CSTRChemIA.api.app:app --reload --port 8000
"""

from __future__ import annotations

from typing import Any

try:
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import RedirectResponse
except ImportError: # pragma: no cover
raise RuntimeError(
"FastAPI não instalado. Instale: pip install -e .[api]"
)

from api.schemas import (
HealthResponse,
JobStatusResponse,
ModelListResponse,
OptimizeRequest,
PredictRequest,
PredictResponse,
)
from core.exceptions import (
CSTRChemIAError,
ModelNotFoundError,
SimulationError,
)
from core.logging import get_logger, setup_logging
from services.model_registry import get_model_registry
from services.optimization_service import OptimizationService
from services.simulation_service import SimulationService

log = get_logger(__name__)


def create_app() -> FastAPI:
"""Factory — usada em testes e no uvicorn."""
setup_logging()

app = FastAPI(
title="CSTRChemIA API",
description="REST API sobre o simulador/otimizador do CSTR realístico.",
version="0.2.0",
)

# Services compartilhados (singletons por processo)
sim_service: SimulationService | None = None
opt_service = OptimizationService()

def _get_sim() -> SimulationService:
nonlocal sim_service
if sim_service is None:
sim_service = SimulationService()
return sim_service

# ---------- endpoints ----------
@app.get("/", include_in_schema=False)
async def root() -> RedirectResponse:
return RedirectResponse(url="/docs")

@app.get("/health", response_model=HealthResponse, tags=["meta"])
async def health() -> HealthResponse:
reg = get_model_registry()
return HealthResponse(
status="ok",
version=app.version,
models_available=reg.available_architectures(),
)

@app.get("/models", response_model=ModelListResponse, tags=["models"])
async def list_models() -> ModelListResponse:
infos = get_model_registry().list_models()
return ModelListResponse(models=[
{
"name": i.name,
"architecture": i.architecture,
"mae": i.mae,
"rmse": i.rmse,
"r2": i.r2,
}
for i in infos
])

@app.post("/predict", response_model=PredictResponse, tags=["inference"])
async def predict(req: PredictRequest) -> PredictResponse:
import time
start = time.perf_counter()
try:
out = _get_sim().predict_named(req.features, model_name=req.model_name)
except ModelNotFoundError as e:
raise HTTPException(status.HTTP_404_NOT_FOUND, str(e)) from e
except (SimulationError, CSTRChemIAError) as e:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) from e
return PredictResponse(
targets=out,
model_name=req.model_name,
wall_time_s=time.perf_counter() - start,
)

@app.post("/optimize", response_model=JobStatusResponse,
status_code=status.HTTP_202_ACCEPTED, tags=["optimization"])
async def optimize(req: OptimizeRequest) -> JobStatusResponse:
try:
job_id = opt_service.submit(
T_max=req.T_max,
X_min=req.X_min,
cat_min=req.cat_min,
fouling_max=req.fouling_max,
cp0_min=req.cp0_min,
pop_size=req.pop_size,
n_gen=req.n_gen,
model_type=req.model_name,
crossover_prob=req.crossover_prob,
mutation_prob=req.mutation_prob,
)
except CSTRChemIAError as e:
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) from e
job = opt_service.get_job(job_id)
assert job is not None
return _job_to_schema(job)

@app.get("/jobs/{job_id}", response_model=JobStatusResponse, tags=["optimization"])
async def get_job(job_id: str) -> JobStatusResponse:
job = opt_service.get_job(job_id)
if job is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "job não encontrado")
return _job_to_schema(job)

return app


def _job_to_schema(job: Any) -> JobStatusResponse:
return JobStatusResponse(
job_id=job.job_id,
status=job.status,
progress={"gen": job.progress_gen, "feasible": job.progress_feasible,
"total_gens": job.total_gens},
started_at=job.started_at,
finished_at=job.finished_at,
error=job.error,
)


app = create_app()
Loading
Loading