diff --git a/lyopronto/pyomo_models/__init__.py b/lyopronto/pyomo_models/__init__.py new file mode 100644 index 0000000..705a703 --- /dev/null +++ b/lyopronto/pyomo_models/__init__.py @@ -0,0 +1,55 @@ +# Copyright (C) 2026, SECQUOIA +# +# This file is part of LyoPRONTO. +# LyoPRONTO is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Pyomo-based optimization models for lyophilization processes. + +This module provides Pyomo-based formulations for lyophilization optimization, +complementing the existing scipy-based optimizers. The Pyomo models offer: + +- Mathematical programming formulations with explicit constraints +- Support for IPOPT and other NLP solvers +- Multi-period trajectory optimization +- Orthogonal collocation discretization + +Usage: + # Install optimization dependencies first: + # pip install .[optimization] + + # If using conda environment, install via: + # conda activate [env_name] + # python -m pip install ".[optimization]" + + from lyopronto.pyomo_models import PYOMO_AVAILABLE + +Actual optimizer implementations will be added in subsequent PRs. + +Note: Requires IPOPT solver. Install via: idaes get-extensions --extra petsc +""" + +from importlib.util import find_spec + + +def _is_pyomo_available() -> bool: + """Return whether the optional Pyomo dependency is importable.""" + return find_spec("pyomo") is not None + + +PYOMO_AVAILABLE = _is_pyomo_available() + +__all__ = ["PYOMO_AVAILABLE"] + +# Version will be set when implementations are added +__version__ = "0.1.0-dev" diff --git a/pyproject.toml b/pyproject.toml index b9428a7..7f0465e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,11 @@ classifiers = [ ] [project.optional-dependencies] +optimization = [ + "pyomo>=6.7.0", + "idaes-pse>=2.5,<2.6; python_version < '3.10'", + "idaes-pse>=2.9.0; python_version >= '3.10'", +] dev = [ "pytest>=7.4.0", "pytest-mock>=3", @@ -58,7 +63,9 @@ docs = [ "mike>=2.1", "mkdocs-ipynb>=0.1.1", ] - +all = [ + "lyopronto[optimization,dev,docs]", +] [project.urls] Homepage = "http://lyopronto.geddes.rcac.purdue.edu" @@ -69,23 +76,71 @@ Documentation = "https://lyohub.github.io/LyoPRONTO/" [tool.setuptools.packages.find] include = ["lyopronto*"] -[tool.pytest.ini_options] -pythonpath = "." -testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -addopts = [ - "-v", - "--strict-markers", - "--tb=short", - "--maxfail=5", - "--cov=lyopronto", - "--cov-report=term-missing", +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +ignore_missing_imports = true +# Exclude pyomo_models from type checking - Pyomo uses dynamic attribute +# assignment (model.Var = pyo.Var(...)) which generates many false-positive +# mypy errors. The code is tested via pytest instead. +exclude = [ + "lyopronto/pyomo_models/", ] -markers = [ - "slow: Tests that take a long time to run", - "fast: Quick tests that run in under 1 second", - "notebook: Tests that execute Jupyter notebooks for documentation", - "main: Tests that cover functionality previously included in main.py", + +[tool.ruff] +# Target Python 3.8+ (matches project.requires-python) +target-version = "py38" +line-length = 88 # Black-compatible +src = ["lyopronto", "tests", "benchmarks"] + +[tool.ruff.lint] +# Enable recommended rules + some extras +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort (import sorting) + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "SIM", # flake8-simplify ] +ignore = [ + "E501", # Line too long (handled by formatter) + "B008", # Do not perform function calls in argument defaults + "SIM108", # Use ternary operator (sometimes less readable) +] + +# Allow autofix for all enabled rules +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.lint.per-file-ignores] +# Tests can use assert and have longer lines +"tests/**/*.py" = ["S101", "E501"] +# Pyomo test files have conditional imports at module level (intentional pattern) +"tests/test_pyomo_models/*.py" = ["E402", "E722", "F401"] +# Pyomo models use closures in loops for rule functions (Pyomo pattern) +"lyopronto/pyomo_models/*.py" = ["B023", "B007", "E722", "SIM102", "F811"] +# Legacy scipy optimizers use closures in loops (legacy pattern) +"lyopronto/opt_*.py" = ["B023"] +# Package __init__ re-exports modules for public API +"lyopronto/__init__.py" = ["F401"] +# Benchmarks may have more complex imports +"benchmarks/**/*.py" = ["E501"] +# Legacy examples may have style issues +"examples/legacy/**/*.py" = ["ALL"] +# Notebooks have different import patterns +"**/*.ipynb" = ["E402", "B007"] +# Test design space has complex warning handling +"tests/test_design_space.py" = ["SIM117"] + +[tool.ruff.format] +# Use double quotes (Black-compatible) +quote-style = "double" +# Indent with spaces +indent-style = "space" +# Unix line endings +line-ending = "lf" diff --git a/tests/test_pyomo_models/test_init.py b/tests/test_pyomo_models/test_init.py new file mode 100644 index 0000000..9e388fe --- /dev/null +++ b/tests/test_pyomo_models/test_init.py @@ -0,0 +1,31 @@ +# Copyright (C) 2026, SECQUOIA + +"""Tests for the Pyomo models package initializer.""" + +from importlib.machinery import ModuleSpec + +from lyopronto import pyomo_models + + +def test_pyomo_available_is_bool(): + assert isinstance(pyomo_models.PYOMO_AVAILABLE, bool) + + +def test_pyomo_available_exported(): + assert pyomo_models.__all__ == ["PYOMO_AVAILABLE"] + + +def test_pyomo_available_false_when_pyomo_missing(monkeypatch): + monkeypatch.setattr(pyomo_models, "find_spec", lambda name: None) + + assert pyomo_models._is_pyomo_available() is False + + +def test_pyomo_available_true_when_pyomo_present(monkeypatch): + monkeypatch.setattr( + pyomo_models, + "find_spec", + lambda name: ModuleSpec(name="pyomo", loader=None), + ) + + assert pyomo_models._is_pyomo_available() is True