Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
68 changes: 58 additions & 10 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ name: PR Tests
# - Draft PRs: Fast tests only (no coverage) for rapid iteration
# - Ready for Review: Full tests with coverage for quality assurance
# - All subsequent commits: Continue with full coverage
# - Pyomo tests run separately and don't block PRs

on:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review, converted_to_draft ]

jobs:
test:
test-scipy:
name: SciPy Tests
runs-on: ubuntu-latest

steps:
Expand All @@ -26,7 +28,7 @@ jobs:
- name: Determine test mode
id: mode
run: |
if [ "${{ github.event.pull_request.draft }}" ]; then
if [ "${{ github.event.pull_request.draft }}" = "true" ]; then
echo "mode=fast" >> $GITHUB_OUTPUT
else
echo "mode=full" >> $GITHUB_OUTPUT
Expand All @@ -48,23 +50,69 @@ jobs:
pip install -e . --no-build-isolation

- name: Run tests
# Currently this conditional branching doesn't actually do anything,
# since pyproject.toml adds these coverage arguments to the testing anyway
run: |
if [ "${{ steps.mode.outputs.mode }}" == "fast" ]; then
echo "Skipping notebook tests (marked with @pytest.mark.notebook) - these run separately"
pytest tests/ -n auto -v -m "not notebook" --cov=lyopronto --cov-report=term-missing
echo "Draft PR: Skipping notebook and pyomo tests for fast feedback"
pytest tests/ -n auto -v -m "not notebook and not pyomo"
else
echo "⚡ Skipping notebook tests (marked with @pytest.mark.slow), not running coverage"
pytest tests/ -n auto -v -m "not notebook"
echo "Ready PR: Running full scipy test suite (excluding notebook and pyomo)"
pytest tests/ -n auto -v -m "not notebook and not pyomo" --cov=lyopronto --cov-report=xml:coverage.xml
fi

- name: Upload coverage (if run)
if: steps.mode.outputs.coverage == 'true'
- name: Upload coverage
if: steps.mode.outputs.mode == 'full'
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: pr-tests
name: pr-coverage
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

test-pyomo:
name: Pyomo Tests (Optional)
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
continue-on-error: true # Pyomo tests are brittle, don't block PRs

steps:
- uses: actions/checkout@v4

- name: Read CI version config
id: versions
uses: mikefarah/yq@v4.44.1
with:
cmd: yq eval '.python-version' .github/ci-config/ci-versions.yml

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ steps.versions.outputs.result }}
cache: 'pip'
cache-dependency-path: |
pyproject.toml

- name: Install dependencies with optimization stack
run: |
python -m pip install --upgrade pip setuptools wheel
pip install .
pip install .[dev]
pip install pyomo idaes-pse
pip install -e . --no-build-isolation

- name: Install IPOPT solver via IDAES
run: |
echo "Installing IPOPT solver via IDAES extensions"
idaes get-extensions --extra petsc

- name: Run Pyomo tests
run: |
echo "Running Pyomo test suite (continue-on-error enabled)"
# Exit code 5 = no tests collected (pyomo tests added in later PRs)
pytest tests/ -n auto -v -m "pyomo" --cov=lyopronto --cov-report=term-missing || { rc=$?; [ $rc -eq 5 ] && echo "No pyomo-marked tests found yet (expected until PR #8)" && exit 0; exit $rc; }

- name: Pyomo Test Summary
if: always()
run: |
echo "Pyomo tests completed"
echo "Failures here don't block PR merge"
47 changes: 38 additions & 9 deletions .github/workflows/slow-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ on:
options:
- 'true'
- 'false'
include_pyomo:
description: 'Include Pyomo tests (requires IPOPT)'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'

jobs:
slow-tests:
Expand All @@ -43,18 +51,39 @@ jobs:
python -m pip install --upgrade pip setuptools wheel
pip install .
pip install .[dev]
if [ "${{ inputs.include_pyomo }}" == "true" ]; then
pip install pyomo idaes-pse
fi
pip install -e . --no-build-isolation

- name: Install IPOPT solver via IDAES (if Pyomo enabled)
if: inputs.include_pyomo == 'true'
run: |
echo "Installing IPOPT solver via IDAES extensions"
idaes get-extensions --extra petsc

- name: Run slow tests
env:
RUN_SLOW_TESTS: "1"
run: |
# Helper: allow exit code 5 (no tests collected) to pass gracefully
run_pytest() { "$@" || { rc=$?; [ $rc -eq 5 ] && echo "No matching tests found (exit 5 is OK)" && return 0; return $rc; }; }
if [ "${{ inputs.run_all }}" == "true" ]; then
echo "🔍 Running ALL tests (including slow optimization tests)"
echo "⏱️ This may take 30-40 minutes on CI"
pytest tests/ -n auto -v --cov=lyopronto --cov-report=xml --cov-report=term-missing
echo "Running ALL tests (including slow optimization tests)"
echo "This may take 30-40 minutes on CI"
if [ "${{ inputs.include_pyomo }}" == "true" ]; then
run_pytest pytest tests/ -n auto -v --cov=lyopronto --cov-report=xml --cov-report=term-missing
else
run_pytest pytest tests/ -n auto -v -m "not pyomo" --cov=lyopronto --cov-report=xml --cov-report=term-missing
fi
else
echo "🐌 Running ONLY slow tests (marked with @pytest.mark.slow)"
echo "⏱️ This focuses on optimization tests that take minutes"
pytest tests/ -n auto -v -m "slow" --cov=lyopronto --cov-report=xml --cov-report=term-missing
echo "Running ONLY slow tests (marked with @pytest.mark.slow)"
echo "This focuses on optimization tests that take minutes"
if [ "${{ inputs.include_pyomo }}" == "true" ]; then
run_pytest pytest tests/ -n auto -v -m "slow" --cov=lyopronto --cov-report=xml --cov-report=term-missing
else
run_pytest pytest tests/ -n auto -v -m "slow and not pyomo" --cov=lyopronto --cov-report=xml --cov-report=term-missing
fi
fi

- name: Upload coverage
Expand All @@ -70,8 +99,8 @@ jobs:
if: always()
run: |
if [ "${{ inputs.run_all }}" == "true" ]; then
echo "Complete test suite finished"
echo "Complete test suite finished"
else
echo "🐌 Slow tests completed"
echo "Slow tests completed"
fi
echo "📊 Coverage uploaded to Codecov"
echo "Coverage uploaded to Codecov"
77 changes: 66 additions & 11 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
name: Main Branch Tests

# Full tests with coverage for main branch
# (PRs are handled by pr-tests.yml)
# Runs both scipy tests and Pyomo tests in separate jobs

on:
push:
branches: [ main, dev-pyomo ]
branches: [ main ]

jobs:
test:
test-scipy:
name: SciPy Tests
runs-on: ubuntu-latest

steps:
Expand All @@ -33,23 +34,77 @@ jobs:
pip install .[dev]
pip install -e . --no-build-isolation

- name: Run ALL tests with pytest and coverage (including slow tests)
- name: Run SciPy tests (excluding Pyomo tests)
run: |
echo "🔍 Running complete test suite including slow tests"
echo "⏱️ This may take 30-40 minutes on CI (includes optimization tests)"
pytest tests/ -n auto -v --cov=lyopronto --cov-report=xml --cov-report=term-missing
echo "Running SciPy test suite (excluding Pyomo tests)"
pytest tests/ -n auto -v -m "not pyomo" --cov=lyopronto --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
flags: scipy-tests
name: scipy-coverage
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

- name: Coverage Summary
if: always()
run: |
echo "✅ Full coverage tests completed for main branch"
echo "📊 Coverage metrics updated in Codecov"
echo "SciPy tests completed for main branch"
echo "Coverage metrics updated in Codecov"

test-pyomo:
name: Pyomo Tests
runs-on: ubuntu-latest
continue-on-error: true # Pyomo tests are brittle, don't block merges

steps:
- uses: actions/checkout@v4
- name: Read CI version config
id: versions
uses: mikefarah/yq@v4.44.1
with:
cmd: yq eval '.python-version' .github/ci-config/ci-versions.yml
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ steps.versions.outputs.result }}
cache: 'pip'
cache-dependency-path: |
pyproject.toml

- name: Install dependencies with optimization stack
run: |
python -m pip install --upgrade pip setuptools wheel
pip install .
pip install .[dev]
pip install pyomo idaes-pse
pip install -e . --no-build-isolation

- name: Install IPOPT solver via IDAES
run: |
echo "Installing IPOPT solver via IDAES extensions"
idaes get-extensions --extra petsc

- name: Run Pyomo tests
run: |
echo "Running Pyomo test suite"
echo "These tests require IPOPT solver and may be slower"
# Exit code 5 = no tests collected (pyomo tests added in later PRs)
pytest tests/ -n auto -v -m "pyomo" --cov=lyopronto --cov-report=xml --cov-report=term-missing || { rc=$?; [ $rc -eq 5 ] && echo "No pyomo-marked tests found yet (expected until PR #8)" && exit 0; exit $rc; }

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: pyomo-tests
name: pyomo-coverage
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

- name: Pyomo Test Summary
if: always()
run: |
echo "Pyomo tests completed (continue-on-error enabled)"
echo "Coverage metrics updated in Codecov"
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@
#
# Python precompiled files
*.pyc
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Data files
*.csv

Expand Down
2 changes: 1 addition & 1 deletion lyopronto/calc_knownRp.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def dry(vial,product,ht,Pchamber,Tshelf,dt):
if Pch_t.max_setpt() > functions.Vapor_pressure(Tsh_t.max_setpt()):
warn("Chamber pressure setpoint exceeds vapor pressure at shelf temperature " +\
"setpoint(s). Drying cannot proceed.")
return np.array([[0.0, Tsh_t(0), Tsh_t(0), Tsh_t(0), Pch_t(0), 0.0, 0.0]])
return np.array([[0.0, Tsh_t(0), Tsh_t(0), Tsh_t(0), Pch_t(0) * 1000.0, 0.0, 0.0]])

inputs = (vial, product, ht, Pch_t, Tsh_t, dt, Lpr0)

Expand Down
16 changes: 8 additions & 8 deletions lyopronto/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
Torr_to_mTorr = 1000.0
cal_To_J = 4.184

rho_ice = 0.918 # g/mL
rho_solute = 1.5 # g/mL
rho_solution = 1.0 # g/mL
rho_ice = 0.918 # [g/mL]
rho_solute = 1.5 # [g/mL]
rho_solution = 1.0 # [g/mL]

dHs = 678.0 # Heat of sublimation in cal/g
k_ice = 0.0059 # Thermal conductivity of ice in cal/cm/s/K
dHf = 79.7 # Heat of fusion in cal/g
dHs = 678.0 # Heat of sublimation [cal/g]
k_ice = 0.0059 # Thermal conductivity of ice [cal/cm/s/K]
dHf = 79.7 # Heat of fusion [cal/g]

Cp_ice = 2030.0 # Constant pressure specific heat of ice in J/kg/K
Cp_solution = 4000.0 # Constant pressure specific heat of water in J/kg/K
Cp_ice = 2030.0 # Constant pressure specific heat of ice [J/kg/K]
Cp_solution = 4000.0 # Constant pressure specific heat of water [J/kg/K]

##################################################
27 changes: 20 additions & 7 deletions lyopronto/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,26 @@ def __init__(self, rampspec, count_ramp_against_dt=True):
times = np.array([0.0, self.dt_setpt[0] / constant.hr_To_min])
# Older logic: setpoint_dt includes the ramp time.
# Kept for backward compatibility, but add a check if insufficient time allowed for ramp
if count_ramp_against_dt:
if count_ramp_against_dt:
# In the no-init path, dt_setpt[0] is already consumed during
# initialization (times starts with [0, dt_setpt[0]]). Start the
# loop index for dt_setpt at i (not i-1) so that dt_setpt[0] is
# not consumed a second time. In the init path, times starts
# with just [0] and the loop correctly starts consuming at
# dt_setpt[0] via index i-1.
has_init = "init" in rampspec
for i in range(1, len(self.setpt)):
# If less dt_setpt than setpt provided, repeat the last dt
totaltime = self.dt_setpt[min(len(self.dt_setpt)-1, i-1)] / constant.hr_To_min
# If fewer dt_setpt than setpt provided, repeat the last dt
dt_idx = i - 1 if has_init else i
totaltime = self.dt_setpt[min(len(self.dt_setpt) - 1, dt_idx)] / constant.hr_To_min
ramptime = abs((self.setpt[i] - self.setpt[i-1]) / self.ramp_rate) / constant.hr_To_min
holdtime = totaltime - ramptime
if ramptime > holdtime:
warn(f"Ramp time from {self.setpt[i-1]:.2e} to {self.setpt[i]:.2e} exceeds total time for setpoint change, {totaltime}.")
if holdtime < 0:
warn(f"Ramp time ({ramptime * constant.hr_To_min:.1f} min) from "
f"{self.setpt[i-1]:.2e} to {self.setpt[i]:.2e} exceeds "
f"total stage time ({totaltime * constant.hr_To_min:.1f} min). "
f"Clamping hold time to 0.")
holdtime = 0.0
times = np.append(times, [ramptime, holdtime])
else:
# Newer logic: setpoint_dt applies *after* the ramp is complete.
Expand Down Expand Up @@ -404,8 +416,9 @@ def fill_output(sol, inputs):
interp_func = PchipInterpolator(sol.t, interp_points, axis=0)
fullout = np.zeros((len(out_t), 7))
for i, t in enumerate(out_t):
if np.any(sol.t == t):
fullout[i,:] = interp_points[sol.t == t, :]
mask = sol.t == t
if np.any(mask):
fullout[i,:] = interp_points[mask, :][0]
else:
fullout[i,:] = interp_func(t)
return fullout
Loading