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
55 changes: 55 additions & 0 deletions lyopronto/pyomo_models/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

"""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"
93 changes: 74 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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"
31 changes: 31 additions & 0 deletions tests/test_pyomo_models/test_init.py
Original file line number Diff line number Diff line change
@@ -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
Loading